001    /*
002     * Databinder: a simple bridge from Wicket to Hibernate
003     * Copyright (C) 2006  Nathan Hamblen nathan@technically.us
004     *
005     * This library is free software; you can redistribute it and/or
006     * modify it under the terms of the GNU Lesser General Public
007     * License as published by the Free Software Foundation; either
008     * version 2.1 of the License, or (at your option) any later version.
009     * 
010     * This library is distributed in the hope that it will be useful,
011     * but WITHOUT ANY WARRANTY; without even the implied warranty of
012     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013     * Lesser General Public License for more details.
014     * 
015     * You should have received a copy of the GNU Lesser General Public
016     * License along with this library; if not, write to the Free Software
017     * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
018     */
019    package net.databinder.auth.components;
020    
021    import java.math.BigInteger;
022    import java.security.GeneralSecurityException;
023    import java.security.KeyPair;
024    import java.security.KeyPairGenerator;
025    import java.security.NoSuchAlgorithmException;
026    import java.security.SecureRandom;
027    import java.security.interfaces.RSAPublicKey;
028    
029    import javax.crypto.Cipher;
030    
031    import net.databinder.auth.valid.EqualPasswordConvertedInputValidator;
032    
033    import org.apache.wicket.ResourceReference;
034    import org.apache.wicket.WicketRuntimeException;
035    import org.apache.wicket.behavior.AttributeAppender;
036    import org.apache.wicket.markup.MarkupStream;
037    import org.apache.wicket.markup.html.IHeaderContributor;
038    import org.apache.wicket.markup.html.IHeaderResponse;
039    import org.apache.wicket.markup.html.form.Form;
040    import org.apache.wicket.markup.html.form.PasswordTextField;
041    import org.apache.wicket.markup.html.resources.JavascriptResourceReference;
042    import org.apache.wicket.model.AbstractReadOnlyModel;
043    import org.apache.wicket.model.IModel;
044    import org.apache.wicket.model.Model;
045    import org.apache.wicket.util.convert.ConversionException;
046    import org.apache.wicket.util.crypt.Base64;
047    
048    /**
049     * Note: if equal password validation is need, use EqualPasswordConvertedInputValidator.
050     * Equal password inputs are not equal until converted (decrypted).
051     * 
052     * @see EqualPasswordConvertedInputValidator
053     */
054    public class RSAPasswordTextField extends PasswordTextField implements IHeaderContributor {
055            private static final ResourceReference RSA_JS = new JavascriptResourceReference(
056                            RSAPasswordTextField.class, "RSA.js");
057            private static final ResourceReference BARRETT_JS = new JavascriptResourceReference(
058                            RSAPasswordTextField.class, "Barrett.js");
059            private static final ResourceReference BIGINT_JS = new JavascriptResourceReference(
060                            RSAPasswordTextField.class, "BigInt.js");
061    
062            private String challenge;
063            
064            /** 1024 bit RSA key, generated on first access. */
065            private static KeyPair keypair;
066            static {
067                    try {
068                            keypair = KeyPairGenerator.getInstance("RSA").genKeyPair();
069                    } catch (NoSuchAlgorithmException e) {
070                            throw new WicketRuntimeException("Can't find RSA provider", e);
071                    }
072            }
073            public RSAPasswordTextField(String id, Form form) {
074                    super(id);
075                    init(form);
076            }
077            public RSAPasswordTextField(String id, IModel model, Form form) {
078                    super(id, model);
079                    init(form);
080            }
081            @Override
082            protected void onRender(MarkupStream markupStream) {
083                    getResponse().write("<noscript><div style='color: red;'>Please enable JavaScript and reload this page.</div></noscript>");
084                    super.onRender(markupStream);
085                    getResponse().write("<script>document.getElementById('" + getMarkupId() + "').style.visibility='visible';</script>");
086            }
087            
088            protected void init(Form form) {
089                    setOutputMarkupId(true);
090                    
091                    add(new AttributeAppender("style", new Model("visibility:hidden"), ";"));
092    
093                    form.add(new AttributeAppender("onsubmit", new AbstractReadOnlyModel() {
094                            public Object getObject() {
095                                    StringBuilder eventBuf = new StringBuilder();
096                                    eventBuf
097                                            .append("if (")
098                                            .append(getElementValue())
099                                            .append(" != null && ")
100                                            .append(getElementValue())
101                                            .append(" != '') ")
102                                            .append(getElementValue())
103                                            .append(" = encryptedString(key, ")
104                                            .append(getChallengeVar())
105                                            .append("+ '|' + ")
106                                            .append(getElementValue())
107                                            .append(");");
108    
109                                    return eventBuf.toString();
110                            }
111                    }, ""));
112                    
113                    challenge = new String(Base64.encodeBase64(
114                                    BigInteger.valueOf(new SecureRandom().nextLong()).toByteArray()));
115            }
116            
117            @Override
118            protected Object convertValue(String[] value) throws ConversionException {
119                    String enc = (String) super.convertValue(value);
120                    if (enc == null)
121                            return null;
122                    try {
123                            Cipher rsa = Cipher.getInstance("RSA");
124                            rsa.init(Cipher.DECRYPT_MODE, keypair.getPrivate());
125                            String dec = new String(rsa.doFinal(hex2data(enc)));
126                            
127                            String[] toks = dec.split("\\|", 2);
128                            if (toks.length != 2 || !toks[0].equals(challenge))
129                                    throw new ConversionException("incorrect or empy challenge value").setResourceKey("RSAPasswordTextField.failed.challenge");
130    
131                            return toks[1];
132                    } catch (GeneralSecurityException e) {
133                            throw new ConversionException(e).setResourceKey("RSAPasswordTextField.failed.challenge");
134                    }
135            }
136            
137            public void renderHead(IHeaderResponse response) {
138                    response.renderJavascriptReference(BIGINT_JS);
139                    response.renderJavascriptReference(BARRETT_JS);
140                    response.renderJavascriptReference(RSA_JS);
141    
142            RSAPublicKey pub= (RSAPublicKey)keypair.getPublic();
143                    StringBuilder keyBuf = new StringBuilder();
144    
145                    // the key is unique per app instance, send once
146                    keyBuf
147                            .append("setMaxDigits(131);\nvar key= new RSAKeyPair('")
148                            .append(pub.getPublicExponent().toString(16))
149                            .append("', '', '")
150                            .append(pub.getModulus().toString(16))
151                            .append("');");
152                    response.renderJavascript(keyBuf.toString(), "rsa_key");
153                    
154                    // the challenge is unique per component instance, send for every component
155                    StringBuilder chalBuf = new StringBuilder();
156                    chalBuf
157                            .append("var ")
158                            .append(getChallengeVar())
159                            .append(" = '")
160                            .append(challenge)
161                            .append("';");
162                    response.renderJavascript(chalBuf.toString(), null);
163            }
164            
165            protected String getChallengeVar() {
166                    return (getMarkupId() + "_challenge");
167            }
168            
169            protected String getElementValue() {
170                    return "document.getElementById('" + getMarkupId() + "').value ";
171            }
172            
173            // these two functions LGPL, origin:
174            //       C-JDBC: Clustered JDBC.
175            //       Copyright (C) 2002-2004 French National Institute For Research In Computer
176            //       Science And Control (INRIA).
177            //       Contact: c-jdbc@objectweb.org
178            // could be replaced by org.apache.commons.codec.binary.Hex
179            private static final byte[] hex2data(String str)
180            {
181                    if (str == null)
182                            return new byte[0];
183    
184                    int len = str.length();
185                    char[] hex = str.toCharArray();
186                    byte[] buf = new byte[len / 2];
187    
188                    for (int pos = 0; pos < len / 2; pos++)
189                            buf[pos] = (byte) (((toDataNibble(hex[2 * pos]) << 4) & 0xF0) | (toDataNibble(hex[2 * pos + 1]) & 0x0F));
190    
191                    return buf;
192            }
193            private  static byte toDataNibble(char c)
194            {
195                    if (('0' <= c) && (c <= '9'))
196                            return (byte) ((byte) c - (byte) '0');
197                    else if (('a' <= c) && (c <= 'f'))
198                            return (byte) ((byte) c - (byte) 'a' + 10);
199                    else if (('A' <= c) && (c <= 'F'))
200                            return (byte) ((byte) c - (byte) 'A' + 10);
201                    else
202                            return -1;
203            }
204    }