Click here to Skip to main content
11,495,331 members (36,749 online)
Click here to Skip to main content

HttpSecureCookie, A Way to Encrypt Cookies with ASP.NET 2.0

, 3 Apr 2006 Ms-PL 195.2K 3.1K 110
Rate this:
Please Sign up or sign in to vote.
Discussing how to encode and tamper-proof text and cookies using the MachineKey, by using reflection.

Introduction

I really have some good laughs when I tamper with cookies on my machine and watch the results when it is submitted back to the site. On the other hand, I don’t want any one to do the same to the cookies that I make!

Cookies, most of the times, shouldn’t be in plain text, at least, they should be tamper-proof! Revealing the content of your cookies might give curious and malicious people an idea about your application’s architecture, and that might help hacking it.

ASP.NET encodes and hashes its authorization ticket, making it secure and tamper-proof. However, the methods used to secure authorization cookies are inaccessible from outside the .NET framework libraries, so you can’t protect your own cookie using these methods; you need to protect it yourself using your own encryption key, encoding and hashing algorithms. HttpSecureCookie works around this by accessing the same methods ASP.NET uses for cookie authorization.

Of course, you shouldn’t save valuable information in your cookies, but if you have to, then this library is at your disposal.

Background

Before you start using this code, if you do not know what MachineKey is, I highly recommend checking this MSDN article: How To: Configure MachineKey in ASP.NET 2.0.

ASP.NET uses the System.Web.Security.CookieProtectionHelper internal class to decode and encode the content of a cookie before submitting it to the client. This class is based on the MachineKey. I wonder why Microsoft kept this class internal!?

To be able to access this internal class, I had to use reflection to be able to access the Decode and Encode methods of CookieProtectionHelper.

Eric Newton has a similar and good article on CP: Encrypting cookies to prevent tampering. However, that code is made for .NET 1.1 and it doesn't work with .NET 2.0 (but it does with some modifications); moreover, its resulting cipher text is in binary format versus being in encrypted format, and I don't know if this is a security risk. Also, I am accessing a higher level class System.Web.Security.CookieProtectionHelper than the one used by that article, System.Web.Configuration.MachineKey, to obtain the cryptography service, and that saved me time by not writing some low level code.

There is also another available method for encoding cookies, by using the FormsAuthenticationTicket and FormsAuthentication.Encrypt; for more information, check the section "Creating the Forms Authentication Cookie" on Explained: Forms Authentication in ASP.NET 2.0. However, I believe, the method mentioned in this article is more flexible.

Obtaining Reference to the CookieProtectionHelper Class via Reflection

To be able to access System.Web.Security.CookieProtectionHelper, I had to create a wrapper class CookieProtectionHelperWrapper which uses reflection to obtain a reference to the underlying methods and exposes the same methods as of the original class:

public static class CookieProtectionHelperWrapper {

    private static MethodInfo _encode;
    private static MethodInfo _decode;

    static CookieProtectionHelperWrapper() {
        // obtaining a reference to System.Web assembly
        Assembly systemWeb = typeof(HttpContext).Assembly;
        if (systemWeb == null) {
            throw new InvalidOperationException(
                "Unable to load System.Web.");
        }
        // obtaining a reference to the internal class CookieProtectionHelper
        Type cookieProtectionHelper = systemWeb.GetType(
                "System.Web.Security.CookieProtectionHelper");
        if (cookieProtectionHelper == null) {
            throw new InvalidOperationException(
                "Unable to get the internal class CookieProtectionHelper.");
        }
        // obtaining references to the methods of CookieProtectionHelper class
        _encode = cookieProtectionHelper.GetMethod(
                "Encode", BindingFlags.NonPublic | BindingFlags.Static);
        _decode = cookieProtectionHelper.GetMethod(
                "Decode", BindingFlags.NonPublic | BindingFlags.Static);

        if (_encode == null || _decode == null) {
            throw new InvalidOperationException(
                "Unable to get the methods to invoke.");
        }
    }

    public static string Encode(CookieProtection cookieProtection, 
                                byte[] buf, int count) {
        return (string)_encode.Invoke(null, 
                new object[] { cookieProtection, buf, count });
    }

    public static byte[] Decode(CookieProtection cookieProtection, 
                                string data) {
        return (byte[])_decode.Invoke(null, 
                new object[] { cookieProtection, data });
    }

}

MachineKeyCryptography: A Cryptography Class Based on MachineKey

MachineKeyCryptography is a static class that provides text ciphering and tamper-proofing services, it provides higher level access to CookieProtectionHelperWrapper. So, if you want to cipher any text based on the machine key, this class is the right one to use.

public static string Encode(string text, CookieProtection cookieProtection) {
    if (string.IsNullOrEmpty(text) || cookieProtection == CookieProtection.None) {
        return text;
    }
    byte[] buf = Encoding.UTF8.GetBytes(text);
    return CookieProtectionHelperWrapper.Encode(cookieProtection, buf, buf.Length); 
}

public static string Decode(string text, CookieProtection cookieProtection) {
    if (string.IsNullOrEmpty(text)) {
        return text;
    }
    byte[] buf;
    try {
        buf = CookieProtectionHelperWrapper.Decode(cookieProtection, text);
    }
    catch(Exception ex) {
        throw new InvalidCypherTextException(
            "Unable to decode the text", ex.InnerException);
    }
    if (buf == null || buf.Length == 0) {
        throw new InvalidCypherTextException(
            "Unable to decode the text");
    }
    return Encoding.UTF8.GetString(buf, 0, buf.Length);
}

HttpSecureCookie Class

This static class will handle the service of securing the content of a cookie. Also, it provides a service to clone a cookie. This class uses MachineKeyCryptography internally to provide crypting services:

public static class HttpSecureCookie {

    public static HttpCookie Encode(HttpCookie cookie) {
        return Encode(cookie, CookieProtection.All);
    }

    public static HttpCookie Encode(HttpCookie cookie, 
                  CookieProtection cookieProtection) {
        HttpCookie encodedCookie = CloneCookie(cookie);
        encodedCookie.Value = 
          MachineKeyCryptography.Encode(cookie.Value, cookieProtection);
        return encodedCookie;
    }

    public static HttpCookie Decode(HttpCookie cookie) {
        return Decode(cookie, CookieProtection.All);
    }

    public static HttpCookie Decode(HttpCookie cookie, 
                  CookieProtection cookieProtection) {
        HttpCookie decodedCookie = CloneCookie(cookie);
        decodedCookie.Value = 
          MachineKeyCryptography.Decode(cookie.Value, cookieProtection);
        return decodedCookie;
    }

    public static HttpCookie CloneCookie(HttpCookie cookie) {
        HttpCookie clonedCookie = new HttpCookie(cookie.Name, cookie.Value);
        clonedCookie.Domain = cookie.Domain;
        clonedCookie.Expires = cookie.Expires;
        clonedCookie.HttpOnly = cookie.HttpOnly;
        clonedCookie.Path = cookie.Path;
        clonedCookie.Secure = cookie.Secure;

        return clonedCookie;
    }
}

Using the Code

Using HttpSecureCookie is easy; for a complete demo, please check the sample application. To encode a cookie:

HttpCookie cookie = new HttpCookie("UserName", "Terminator");
cookie.Expires = DateTime.Now.AddDays(1);
HttpCookie encodedCookie = HttpSecureCookie.Encode(cookie);
Response.Cookies.Add(encodedCookie);

To decode an encoded cookie:

HttpCookie cookie = Request.Cookies["UserName"];
lblDisplayBefore.Text = cookie.Value;
HttpCookie decodedCookie = HttpSecureCookie.Decode(cookie);

To use HttpSecureCookie on a web farm, you need to set the correct MachineKey configuration in Web.Config. For more information, check the "Web Farm Deployment Considerations" section on How To: Configure MachineKey in ASP.NET 2.0. To generate a machine key, please check How to create keys by using Visual C# .NET for use in Forms authentication.

Limitations

This library uses reflection, so it might break with the next version of .NET. Also, it doesn't work with .NET 1.1.

Reflection might have some performance implication; however, I used an assembly that is already loaded, "System.Web.dll", and I am only using reflection once across the life time of the application, to gain extra performance.

Conclusion

If you don't want a sophisticated cookie encryption service, if you don't want to mind the encryption key, and if you don't want to create your own encryption algorithm, then this library is for you!

Comments and suggestions are welcome. Please vote if you like this article (or if you didn't).

History

  • 1 April 2006 - First version.

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)

Share

About the Author

Adam Tibi
Architect
United Kingdom United Kingdom
Passionate about refining software practices, promoting self-motivated teams and orchestrating agile projects that hit the deadline.
Lives in London, UK and works as a .NET architect consultant in the City.

Need a pro service to help your organisation? Contact me via my website www.AdamTibi.net.
Follow on   Twitter

Comments and Discussions

 
QuestionBug on 4.5 framework ? Pin
diego mesa tabares5-Oct-12 7:29
memberdiego mesa tabares5-Oct-12 7:29 
AnswerRe: Bug on 4.5 framework ? Pin
Adam Tibi5-Oct-12 17:19
memberAdam Tibi5-Oct-12 17:19 
GeneralRe: Bug on 4.5 framework ? Pin
Jordi Corominas11-Jul-13 22:10
memberJordi Corominas11-Jul-13 22:10 
BugRe: Bug on 4.5 framework ? Pin
heroes003-Dec-13 21:30
memberheroes003-Dec-13 21:30 
GeneralRe: Bug on 4.5 framework ? Pin
Adam Tibi4-Dec-13 2:19
memberAdam Tibi4-Dec-13 2:19 
SuggestionInternal class and reflection Pin
vasekz11-Feb-12 8:33
membervasekz11-Feb-12 8:33 
GeneralRe: Internal class and reflection Pin
Adam Tibi13-Feb-12 11:41
memberAdam Tibi13-Feb-12 11:41 
GeneralRe: Internal class and reflection Pin
JasonJoh3-Aug-12 1:44
memberJasonJoh3-Aug-12 1:44 
GeneralNot update cookie instantly Pin
ashutosh kumar jha20-May-11 1:15
memberashutosh kumar jha20-May-11 1:15 
GeneralNot working reliably in a web farm, even though machineKeys are synced Pin
frankosaurus3-Dec-10 17:03
memberfrankosaurus3-Dec-10 17:03 
GeneralRe: Not working reliably in a web farm, even though machineKeys are synced Pin
frankosaurus5-Dec-10 15:10
memberfrankosaurus5-Dec-10 15:10 
GeneralWas working great - now throwing errors Pin
Chuck Bevitt4-Nov-10 14:42
memberChuck Bevitt4-Nov-10 14:42 
I've used this for a couple of years on my web apps; it's been working great. Suddenly, a month or so ago, it started occasionally throwing errors. This only happens 5 to 15 times a day, on a site with thousands of hits per day.

Before anyone asks, yes, the web site is on a server farm and I do specify the machine key in the web config file. I haven't made any changes that could account for this.

Here are a couple of the errors from our error notification emails, which unwind the inner exceptions:

Error Ticket is: VSIIS1_11-04-2010___3

Message(0): /Login.aspx, IP: 209.99.19.8, Raw User Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727; InfoPath.2; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)

Stack Trace(0):

Source(0): Error.aspx

Message(1): Exception of type 'System.Web.HttpUnhandledException' was thrown.

Stack Trace(1): at System.Web.UI.Page.HandleError(Exception e)
at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)
at System.Web.UI.Page.ProcessRequest(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)
at System.Web.UI.Page.ProcessRequest()
at System.Web.UI.Page.ProcessRequestWithNoAssert(HttpContext context)
at System.Web.UI.Page.ProcessRequest(HttpContext context)
at ASP.login_aspx.ProcessRequest(HttpContext context)
at System.Web.HttpApplication.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)

Source(1): System.Web

Message(2): Unable to decode the text

Stack Trace(2): at AdamTibi.Web.Security.MachineKeyCryptography.Decode(String text, CookieProtection cookieProtection)
at AdamTibi.Web.Security.HttpSecureCookie.Decode(HttpCookie cookie, CookieProtection cookieProtection)
at AdamTibi.Web.Security.HttpSecureCookie.Decode(HttpCookie cookie)
at Global.get_AccessAccountID()
at Global.SetPreferenceCookies()
at _Default.Authenticate(String cUser, String cPass, String& cFailureText)
at _Default.Login1_Authenticate(Object sender, AuthenticateEventArgs e)
at System.Web.UI.WebControls.Login.OnAuthenticate(AuthenticateEventArgs e)
at System.Web.UI.WebControls.Login.AttemptLogin()
at System.Web.UI.WebControls.Login.OnBubbleEvent(Object source, EventArgs e)
at System.Web.UI.Control.RaiseBubbleEvent(Object source, EventArgs args)
at System.Web.UI.WebControls.Button.OnCommand(CommandEventArgs e)
at System.Web.UI.WebControls.Button.RaisePostBackEvent(String eventArgument)
at System.Web.UI.WebControls.Button.System.Web.UI.IPostBackEventHandler.RaisePostBackEvent(String eventArgument)
at System.Web.UI.Page.RaisePostBackEvent(IPostBackEventHandler sourceControl, String eventArgument)
at System.Web.UI.Page.RaisePostBackEvent(NameValueCollection postData)
at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)

Source(2): AdamTibi.Web.Security

Message(3): Unable to validate data.

Stack Trace(3): at System.Web.Configuration.MachineKeySection.EncryptOrDecryptData(Boolean fEncrypt, Byte[] buf, Byte[] modifier, Int32 start, Int32 length, IVType ivType, Boolean useValidationSymAlgo, Boolean signData)
at System.Web.Configuration.MachineKeySection.EncryptOrDecryptData(Boolean fEncrypt, Byte[] buf, Byte[] modifier, Int32 start, Int32 length, IVType ivType, Boolean useValidationSymAlgo)
at System.Web.Security.CookieProtectionHelper.Decode(CookieProtection cookieProtection, String data)

Source(3): System.Web



Error Ticket is: VSIIS2_11-04-2010___2

Message(0): /Login.aspx, IP: 206.216.34.171, Raw User Agent: Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648)

Stack Trace(0):

Source(0): Error.aspx

Message(1): Exception of type 'System.Web.HttpUnhandledException' was thrown.

Stack Trace(1): at System.Web.UI.Page.HandleError(Exception e)
at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)
at System.Web.UI.Page.ProcessRequest(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)
at System.Web.UI.Page.ProcessRequest()
at System.Web.UI.Page.ProcessRequestWithNoAssert(HttpContext context)
at System.Web.UI.Page.ProcessRequest(HttpContext context)
at ASP.login_aspx.ProcessRequest(HttpContext context)
at System.Web.HttpApplication.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)

Source(1): System.Web

Message(2): Unable to decode the text

Stack Trace(2): at AdamTibi.Web.Security.MachineKeyCryptography.Decode(String text, CookieProtection cookieProtection)
at AdamTibi.Web.Security.HttpSecureCookie.Decode(HttpCookie cookie, CookieProtection cookieProtection)
at AdamTibi.Web.Security.HttpSecureCookie.Decode(HttpCookie cookie)
at Global.get_AccessAccountID()
at Global.SetPreferenceCookies()
at _Default.Authenticate(String cUser, String cPass, String& cFailureText)
at _Default.Login1_Authenticate(Object sender, AuthenticateEventArgs e)
at System.Web.UI.WebControls.Login.OnAuthenticate(AuthenticateEventArgs e)
at System.Web.UI.WebControls.Login.AttemptLogin()
at System.Web.UI.WebControls.Login.OnBubbleEvent(Object source, EventArgs e)
at System.Web.UI.Control.RaiseBubbleEvent(Object source, EventArgs args)
at System.Web.UI.WebControls.Button.OnCommand(CommandEventArgs e)
at System.Web.UI.WebControls.Button.RaisePostBackEvent(String eventArgument)
at System.Web.UI.WebControls.Button.System.Web.UI.IPostBackEventHandler.RaisePostBackEvent(String eventArgument)
at System.Web.UI.Page.RaisePostBackEvent(IPostBackEventHandler sourceControl, String eventArgument)
at System.Web.UI.Page.RaisePostBackEvent(NameValueCollection postData)
at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)

Source(2): AdamTibi.Web.Security

Message(3): Length of the data to decrypt is invalid.

Stack Trace(3): at System.Security.Cryptography.RijndaelManagedTransform.TransformFinalBlock(Byte[] inputBuffer, Int32 inputOffset, Int32 inputCount)
at System.Security.Cryptography.CryptoStream.FlushFinalBlock()
at System.Web.Configuration.MachineKeySection.EncryptOrDecryptData(Boolean fEncrypt, Byte[] buf, Byte[] modifier, Int32 start, Int32 length, IVType ivType, Boolean useValidationSymAlgo)
at System.Web.Security.CookieProtectionHelper.Decode(CookieProtection cookieProtection, String data)

Source(3): mscorlib


I thought at first that we were starting to get hits from mobile devices that were causing the problem; adding the raw user agent to the error notification proved that that is not the case.

My best guess is that some Microsoft patch may have caused the problem.

It's running on IIS6 on Windows Server 2003 (virtual machines) and ASP.Net 2.0.

If anyone has any ideas, I'd really appreciate it. Remember, the problem only occurs a few times each day - usually it works fine.

Anyway, thanks for the library - it's worked great up to now.
GeneralRe: Was working great - now throwing errors Pin
moabi K15-May-11 18:55
membermoabi K15-May-11 18:55 
GeneralRe: Was working great - now throwing errors Pin
Chuck Bevitt16-May-11 7:18
memberChuck Bevitt16-May-11 7:18 
GeneralRe: Was working great - now throwing errors Pin
diego mesa tabares29-Sep-11 12:58
memberdiego mesa tabares29-Sep-11 12:58 
GeneralI'm pretty sure I'm missing something, but... Pin
reinux1-Nov-10 22:26
memberreinux1-Nov-10 22:26 
GeneralRe: I'm pretty sure I'm missing something, but... Pin
frankosaurus5-Dec-10 15:06
memberfrankosaurus5-Dec-10 15:06 
GeneralRe: I'm pretty sure I'm missing something, but... Pin
reinux5-Dec-10 15:46
memberreinux5-Dec-10 15:46 
GeneralThanks for the props Pin
ericnewton769-Jul-10 8:12
memberericnewton769-Jul-10 8:12 
GeneralMedium Trust Pin
Spiff Dog1-Mar-10 11:40
memberSpiff Dog1-Mar-10 11:40 
GeneralRe: Medium Trust Pin
Adam Tibi2-Mar-10 1:12
memberAdam Tibi2-Mar-10 1:12 
GeneralThanks for the insight Adam...I've actually implemented your code in my project Pin
Ziad J.khan26-Nov-09 11:45
memberZiad J.khan26-Nov-09 11:45 
GeneralRe: Thanks for the insight Adam...I've actually implemented your code in my project Pin
Adam Tibi30-Nov-09 3:20
memberAdam Tibi30-Nov-09 3:20 
Generalerror : padding is invalid... Pin
jamel197416-Sep-08 5:39
memberjamel197416-Sep-08 5:39 
GeneralRe: error : padding is invalid... Pin
Adam Tibi16-Sep-08 12:54
memberAdam Tibi16-Sep-08 12:54 
GeneralRe: error : padding is invalid... Pin
jamel197416-Sep-08 22:03
memberjamel197416-Sep-08 22:03 
GeneralRe: error : padding is invalid... Pin
jamel197416-Sep-08 23:21
memberjamel197416-Sep-08 23:21 
GeneralInvalidCypherText.... no good. Pin
KurtPW29-Aug-08 14:31
memberKurtPW29-Aug-08 14:31 
GeneralRe: InvalidCypherText.... no good. Pin
Adam Tibi31-Aug-08 12:47
memberAdam Tibi31-Aug-08 12:47 
GeneralThanks Adam Pin
lehuuduc6-Aug-08 18:37
memberlehuuduc6-Aug-08 18:37 
GeneralRe: Thanks Adam Pin
Adam Tibi7-Aug-08 0:22
memberAdam Tibi7-Aug-08 0:22 
GeneralDownload link error [modified] Pin
Marie-Christine Bechara6-May-08 3:52
memberMarie-Christine Bechara6-May-08 3:52 
GeneralRe: Download link error Pin
Adam Tibi9-May-08 0:05
memberAdam Tibi9-May-08 0:05 
GeneralPadding is invalid and cannot be removed. Pin
Surjit Singh23-Apr-08 1:00
memberSurjit Singh23-Apr-08 1:00 
GeneralRe: Padding is invalid and cannot be removed. Pin
Adam Tibi24-Apr-08 23:32
memberAdam Tibi24-Apr-08 23:32 
QuestionClarification Pin
stixoffire1-Apr-08 23:57
memberstixoffire1-Apr-08 23:57 
Generalthank you Pin
Rob van der Veer25-Feb-08 5:38
memberRob van der Veer25-Feb-08 5:38 
GeneralRe: thank you Pin
Adam Tibi25-Feb-08 23:04
memberAdam Tibi25-Feb-08 23:04 
QuestionHow do you delete the cookie? Pin
Simon Deshaies10-Dec-07 7:11
memberSimon Deshaies10-Dec-07 7:11 
QuestionInvalidCypherTextException can't find? Pin
dianlongliu27-Nov-07 15:18
memberdianlongliu27-Nov-07 15:18 
AnswerRe: InvalidCypherTextException can't find? Pin
Adam Tibi28-Nov-07 0:45
memberAdam Tibi28-Nov-07 0:45 
GeneralRe: InvalidCypherTextException can't find? Pin
dianlongliu28-Nov-07 4:30
memberdianlongliu28-Nov-07 4:30 
GeneralRe: InvalidCypherTextException can't find? Pin
Adam Tibi29-Nov-07 0:03
memberAdam Tibi29-Nov-07 0:03 
GeneralRe: InvalidCypherTextException can't find? Pin
dianlongliu29-Nov-07 15:55
memberdianlongliu29-Nov-07 15:55 
GeneralRe: InvalidCypherTextException can't find? Pin
dianlongliu29-Nov-07 16:00
memberdianlongliu29-Nov-07 16:00 
GeneralRe: InvalidCypherTextException can't find? Pin
Adam Tibi30-Nov-07 0:39
memberAdam Tibi30-Nov-07 0:39 
GeneralRe: InvalidCypherTextException can't find? Pin
dianlongliu30-Nov-07 4:10
memberdianlongliu30-Nov-07 4:10 
QuestionPadding exception Pin
DaberElay9-Sep-07 23:07
memberDaberElay9-Sep-07 23:07 
AnswerRe: Padding exception Pin
Adam Tibi9-Sep-07 23:30
memberAdam Tibi9-Sep-07 23:30 
AnswerRe: Padding exception Pin
Boxcarwilli15-Sep-07 20:37
memberBoxcarwilli15-Sep-07 20:37 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Terms of Use | Mobile
Web01 | 2.8.150520.1 | Last Updated 3 Apr 2006
Article Copyright 2006 by Adam Tibi
Everything else Copyright © CodeProject, 1999-2015
Layout: fixed | fluid