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 }