/* * Databinder: a simple bridge from Wicket to Hibernate * Copyright (C) 2006 Nathan Hamblen nathan@technically.us * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ package net.databinder.auth.components; import java.math.BigInteger; import java.security.GeneralSecurityException; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.interfaces.RSAPublicKey; import javax.crypto.Cipher; import net.databinder.auth.valid.EqualPasswordConvertedInputValidator; import org.apache.wicket.ResourceReference; import org.apache.wicket.WicketRuntimeException; import org.apache.wicket.behavior.AttributeAppender; import org.apache.wicket.markup.MarkupStream; import org.apache.wicket.markup.html.IHeaderContributor; import org.apache.wicket.markup.html.IHeaderResponse; import org.apache.wicket.markup.html.form.Form; import org.apache.wicket.markup.html.form.PasswordTextField; import org.apache.wicket.markup.html.resources.JavascriptResourceReference; import org.apache.wicket.model.AbstractReadOnlyModel; import org.apache.wicket.model.IModel; import org.apache.wicket.model.Model; import org.apache.wicket.util.convert.ConversionException; import org.apache.wicket.util.crypt.Base64; /** * Note: if equal password validation is need, use EqualPasswordConvertedInputValidator. * Equal password inputs are not equal until converted (decrypted). * * @see EqualPasswordConvertedInputValidator */ public class RSAPasswordTextField extends PasswordTextField implements IHeaderContributor { private static final ResourceReference RSA_JS = new JavascriptResourceReference( RSAPasswordTextField.class, "RSA.js"); private static final ResourceReference BARRETT_JS = new JavascriptResourceReference( RSAPasswordTextField.class, "Barrett.js"); private static final ResourceReference BIGINT_JS = new JavascriptResourceReference( RSAPasswordTextField.class, "BigInt.js"); private String challenge; /** 1024 bit RSA key, generated on first access. */ private static KeyPair keypair; static { try { keypair = KeyPairGenerator.getInstance("RSA").genKeyPair(); } catch (NoSuchAlgorithmException e) { throw new WicketRuntimeException("Can't find RSA provider", e); } } public RSAPasswordTextField(String id, Form form) { super(id); init(form); } public RSAPasswordTextField(String id, IModel<String> model, Form form) { super(id, model); init(form); } @Override protected void onRender(MarkupStream markupStream) { getResponse().write("<noscript><div style='color: red;'>Please enable JavaScript and reload this page.</div></noscript>"); super.onRender(markupStream); getResponse().write("<script>document.getElementById('" + getMarkupId() + "').style.visibility='visible';</script>"); } protected void init(Form form) { setOutputMarkupId(true); add(new AttributeAppender("style", new Model<String>("visibility:hidden"), ";")); form.add(new AttributeAppender("onsubmit", new AbstractReadOnlyModel() { public Object getObject() { StringBuilder eventBuf = new StringBuilder(); eventBuf .append("if (") .append(getElementValue()) .append(" != null && ") .append(getElementValue()) .append(" != '') ") .append(getElementValue()) .append(" = encryptedString(key, ") .append(getChallengeVar()) .append("+ '|' + ") .append(getElementValue()) .append(");"); return eventBuf.toString(); } }, "")); challenge = new String(Base64.encodeBase64( BigInteger.valueOf(new SecureRandom().nextLong()).toByteArray())); } @Override protected String convertValue(String[] value) throws ConversionException { String enc = (String) super.convertValue(value); if (enc == null) return null; try { Cipher rsa = Cipher.getInstance("RSA"); rsa.init(Cipher.DECRYPT_MODE, keypair.getPrivate()); String dec = new String(rsa.doFinal(hex2data(enc))); String[] toks = dec.split("\\|", 2); if (toks.length != 2 || !toks[0].equals(challenge)) throw new ConversionException("incorrect or empy challenge value").setResourceKey("RSAPasswordTextField.failed.challenge"); return toks[1]; } catch (GeneralSecurityException e) { throw new ConversionException(e).setResourceKey("RSAPasswordTextField.failed.challenge"); } } public void renderHead(IHeaderResponse response) { response.renderJavascriptReference(BIGINT_JS); response.renderJavascriptReference(BARRETT_JS); response.renderJavascriptReference(RSA_JS); RSAPublicKey pub= (RSAPublicKey)keypair.getPublic(); StringBuilder keyBuf = new StringBuilder(); // the key is unique per app instance, send once keyBuf .append("setMaxDigits(131);\nvar key= new RSAKeyPair('") .append(pub.getPublicExponent().toString(16)) .append("', '', '") .append(pub.getModulus().toString(16)) .append("');"); response.renderJavascript(keyBuf.toString(), "rsa_key"); // the challenge is unique per component instance, send for every component StringBuilder chalBuf = new StringBuilder(); chalBuf .append("var ") .append(getChallengeVar()) .append(" = '") .append(challenge) .append("';"); response.renderJavascript(chalBuf.toString(), null); } protected String getChallengeVar() { return (getMarkupId() + "_challenge"); } protected String getElementValue() { return "document.getElementById('" + getMarkupId() + "').value "; } // these two functions LGPL, origin: // C-JDBC: Clustered JDBC. // Copyright (C) 2002-2004 French National Institute For Research In Computer // Science And Control (INRIA). // Contact: c-jdbc@objectweb.org // could be replaced by org.apache.commons.codec.binary.Hex private static final byte[] hex2data(String str) { if (str == null) return new byte[0]; int len = str.length(); char[] hex = str.toCharArray(); byte[] buf = new byte[len / 2]; for (int pos = 0; pos < len / 2; pos++) buf[pos] = (byte) (((toDataNibble(hex[2 * pos]) << 4) & 0xF0) | (toDataNibble(hex[2 * pos + 1]) & 0x0F)); return buf; } private static byte toDataNibble(char c) { if (('0' <= c) && (c <= '9')) return (byte) ((byte) c - (byte) '0'); else if (('a' <= c) && (c <= 'f')) return (byte) ((byte) c - (byte) 'a' + 10); else if (('A' <= c) && (c <= 'F')) return (byte) ((byte) c - (byte) 'A' + 10); else return -1; } }