package thaw.plugins.miniFrost.frostKSK;
import java.sql.*;
import org.w3c.dom.*;
import java.text.SimpleDateFormat;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.UnsupportedEncodingException;
import java.util.Vector;
import java.util.Iterator;
import java.util.List;
import java.util.Date;
import frost.util.XMLTools;
import org.bouncycastle.util.encoders.Base64;
import frost.crypt.FrostCrypt;
import thaw.plugins.signatures.Identity;
import thaw.plugins.Hsqldb;
import thaw.core.Logger;
import thaw.core.I18n;
import thaw.plugins.miniFrost.RegexpBlacklist;
/**
* Dirty parser reusing some Frost functions
* (Note: dirty mainly because of the Frost format :p)
*/
public class KSKMessageParser {
private final static SimpleDateFormat simpleFormat = new SimpleDateFormat("yyyy.M.d HH:mm:ss");
private SimpleDateFormat gmtFormat;
public KSKMessageParser() {
gmtFormat = new SimpleDateFormat("yyyy.M.d HH:mm:ss");
gmtFormat.setTimeZone(java.util.TimeZone.getTimeZone("GMT"));
}
private String messageId;
private String inReplyTo;
private String inReplyToFull;
private String from;
private String subject;
private String date;
private String time;
private String recipient;
private String board;
private String body;
private String publicKey;
private String signature;
private String idLinePos;
private String idLineLen;
private String wotPublicKey;
private String wotPublicKeySignature;
private Vector attachments;
private Identity identity;
private boolean read = false;
private boolean archived = false;
private Identity encryptedFor = null;
private static FrostCrypt frostCrypt;
private boolean loaded = false;
public KSKMessageParser(Hsqldb db,
String inReplyTo, /* msg id */
String from,
String subject,
java.util.Date dateUtil,
Identity encryptedFor,
String board,
String body,
String publicKey,
Vector attachments,
Identity identity,
int idLinePos,
int idLineLen,
String wotPublicKey) {
this();
this.messageId = ""; /* will be generated from the SHA1 of the content */
this.inReplyTo = inReplyTo;
this.from = from;
this.subject = subject;
String[] date = simpleFormat.format(dateUtil).toString().split(" ");
this.date = date[0];
this.time = date[1];
this.encryptedFor = encryptedFor;
this.recipient = (encryptedFor != null ? encryptedFor.toString() : null);
this.board = board;
this.body = body;
this.publicKey = publicKey;
this.idLinePos = Integer.toString(idLinePos);
this.idLineLen = Integer.toString(idLineLen);
this.attachments = attachments;
this.identity = identity;
inReplyToFull = getFullInReplyTo(db, inReplyTo);
if (frostCrypt == null)
frostCrypt = new FrostCrypt();
/* frost wants a SHA256 hash, but can't check from what is comes :p */
this.messageId = frostCrypt.computeChecksumSHA256(getSignedContent(false));
this.wotPublicKey = wotPublicKey;
if (identity == null) {
signature = null;
} else {
signature = identity.sign(getSignedContent(true));
}
wotPublicKeySignature = getWotPublicKeySignature(identity, wotPublicKey);
}
private String getWotPublicKeySignature(Identity identity, String wotPublicKey) {
if (identity == null || wotPublicKey == null)
return null;
String toSign = wotPublicKey+"|"+date+"|"+time+"|"+messageId; /* no 'GMT' at the end of the time string because I'm a lazy bastard */
return identity.sign(toSign);
}
private boolean checkWotPublicKeySignature(Identity identity, String wotPublicKey, String signature) {
if (identity == null || wotPublicKey == null || signature == null)
return false;
String toSign = wotPublicKey+"|"+date+"|"+time+"|"+messageId; /* no 'GMT' at the end of the time string because I'm a lazy bastard */
return identity.check(toSign, signature);
}
public Identity getIdentity() {
return identity;
}
public String getTrustListPublicKey() {
return wotPublicKey;
}
private boolean alreadyInTheDb(Hsqldb db, String msgId) {
try {
synchronized(db.dbLock) {
PreparedStatement st;
st = db.getConnection().prepareStatement("SELECT id FROM frostKSKMessages "+
"WHERE msgId = ? LIMIT 1");
st.setString(1, msgId);
ResultSet res = st.executeQuery();
boolean b = (res.next());
st.close();
return b;
}
} catch(SQLException e) {
Logger.error(this,
"Exception while checking if the message was already in the db: "+
e.toString());
return false;
}
}
public java.util.Date getDate() {
date = date.trim();
time = time.trim();
date += " "+time;
java.util.Date dateUtil = null;
try {
dateUtil = simpleFormat.parse(date);
} catch(java.text.ParseException e) {
Logger.notice(this, "Can't parse the date !");
return null;
}
return dateUtil;
}
public boolean insert(Hsqldb db,
int boardId, java.util.Date boardDate, int rev,
String boardNameExpected) {
if (boardNameExpected == null) {
Logger.notice(this, "Board name expected == null ?!");
return false;
}
if (board != null
&& !(boardNameExpected.toLowerCase().equals(board.toLowerCase()))) {
Logger.notice(this, "Board name doesn't match");
return false;
}
if (messageId == null) {
Logger.notice(this, "No message id, can't store.");
return false;
}
if (alreadyInTheDb(db, messageId)) {
Logger.notice(this, "We have already this id in the db ?!");
archived = true;
read = true;
}
date = date.trim();
time = time.trim();
date += " "+time;
java.util.Date dateUtil = null;
try {
dateUtil = simpleFormat.parse(date);
} catch(java.text.ParseException e) {
Logger.notice(this, "Can't parse the date !");
return false;
}
if (dateUtil != null) {
long dateDiff = KSKBoard.getMidnight(dateUtil).getTime() - KSKBoard.getMidnight(boardDate).getTime();
/* we accept between X days before and X days after */
if (dateDiff < (KSKBoard.MAX_DAYS_IN_THE_PAST+1)*(-1)*28*60*60*1000
|| dateDiff > (KSKBoard.MAX_DAYS_IN_THE_FUTURE+1)*24*60*60*1000)
dateUtil = null;
}
java.sql.Timestamp timestampSql;
if (dateUtil != null)
timestampSql = new java.sql.Timestamp(dateUtil.getTime());
else
timestampSql = new java.sql.Timestamp(boardDate.getTime());
java.sql.Date dateSql = new java.sql.Date(boardDate.getTime());
int replyToId = -1;
try {
synchronized(db.dbLock) {
PreparedStatement st;
/* we search the message to this one answer */
if (inReplyTo != null) {
inReplyToFull = inReplyTo;
String[] split = inReplyTo.split(",");
inReplyTo = split[split.length-1];
st = db.getConnection().prepareStatement("SELECT id FROM frostKSKMessages "+
"WHERE msgId = ? LIMIT 1");
st.setString(1, inReplyTo);
ResultSet res = st.executeQuery();
if (res.next())
replyToId = res.getInt("id");
st.close();
}
/* we insert the message */
st = db.getConnection().prepareStatement("INSERT INTO frostKSKMessages ("+
"subject, nick, sigId, content, "+
"date, msgId, inReplyTo, inReplyToId, "+
"rev, keyDate, read, archived, "+
"encryptedFor, boardId) VALUES ("+
"?, ?, ?, ?, "+
"?, ?, ?, ?, "+
"?, ?, ?, ?, "+
"?, ?)");
st.setString(1, subject);
st.setString(2, from); /* nick */
if (identity != null)
st.setInt(3, identity.getId());
else
st.setNull(3, Types.INTEGER);
st.setString(4, body); /* content */
st.setTimestamp(5, timestampSql);
st.setString(6, messageId);
if (replyToId >= 0)
st.setInt(7, replyToId);
else
st.setNull(7, Types.INTEGER);
if (inReplyTo != null)
st.setString(8, inReplyTo);
else
st.setNull(8, Types.VARCHAR);
st.setInt(9, rev);
st.setDate(10, dateSql);
st.setBoolean(11, read);
st.setBoolean(12, archived);
if (encryptedFor == null)
st.setNull(13, Types.INTEGER);
else
st.setInt(13, encryptedFor.getId());
st.setInt(14, boardId);
st.execute();
st.close();
String boardName = (board != null) ? board : "(null)";
Logger.notice(this, "Last inserted message in the db : "+
boardName + " (" + Integer.toString(boardId) + ") - " +
timestampSql.toString() + " - " +
Integer.toString(rev));
/* we need the id of the message */
st = db.getConnection().prepareStatement("SELECT id FROM frostKSKmessages "+
"WHERE msgId = ? LIMIT 1");
st.setString(1, messageId);
ResultSet set = st.executeQuery();
set.next();
int id = set.getInt("id");
st.close();
/* we insert the attachments */
if (attachments != null) {
for(Iterator it = attachments.iterator();
it.hasNext();) {
KSKAttachment a = (KSKAttachment)it.next();
a.insert(db, id);
}
}
}
} catch(SQLException e) {
Logger.warning(this, "Can't insert the message into the db because : "+e.toString());
return false;
}
return true;
}
public boolean filter(RegexpBlacklist blacklist) {
if (!loaded) {
/* message was not loaded and so was replaced by an "invalid message"
* so the message haven't to be filtered
*/
return true;
}
if (blacklist.isBlacklisted(subject)
|| blacklist.isBlacklisted(from)
|| blacklist.isBlacklisted(body)) {
read = true;
archived = true;
}
return true;
}
public final static char SIGNATURE_ELEMENTS_SEPARATOR = '|';
/**
* @param withMsgId require to check the signature
*/
private String getSignedContent(boolean withMsgId) {
final StringBuffer allContent = new StringBuffer();
allContent.append(date).append(SIGNATURE_ELEMENTS_SEPARATOR);
allContent.append(time+"GMT").append(SIGNATURE_ELEMENTS_SEPARATOR);
allContent.append(board).append(SIGNATURE_ELEMENTS_SEPARATOR);
allContent.append(from).append(SIGNATURE_ELEMENTS_SEPARATOR);
if (withMsgId)
allContent.append(messageId).append(SIGNATURE_ELEMENTS_SEPARATOR);
if( inReplyToFull != null && inReplyToFull.length() > 0 ) {
allContent.append(inReplyToFull).append(SIGNATURE_ELEMENTS_SEPARATOR);
}
if( recipient != null && recipient.length() > 0 ) {
allContent.append(recipient).append(SIGNATURE_ELEMENTS_SEPARATOR);
}
allContent.append(idLinePos).append(SIGNATURE_ELEMENTS_SEPARATOR);
allContent.append(idLineLen).append(SIGNATURE_ELEMENTS_SEPARATOR);
allContent.append(subject).append(SIGNATURE_ELEMENTS_SEPARATOR);
allContent.append(body).append(SIGNATURE_ELEMENTS_SEPARATOR);
if (attachments != null) {
for (Iterator it = attachments.iterator();
it.hasNext();) {
KSKAttachment a = (KSKAttachment)it.next();
allContent.append(a.getSignedStr());
}
}
return allContent.toString();
}
public boolean checkSignature(Hsqldb db) {
if (!loaded) {
/* message was not loaded and so was replaced by an "invalid message"
* so the signature have not to be checked
*/
return true;
}
if (publicKey == null || signature == null) {
from = from.replaceAll("@", "_");
return true;
}
String[] split = from.split("@");
if (split.length < 2 || "".equals(split[0].trim())) {
Logger.notice(this, "Unable to extract the nick name from the message");
return false;
}
String nick = split[0].trim();
identity = Identity.getIdentity(db, nick, publicKey);
boolean ret = identity.check(getSignedContent(true), signature);
if (!ret) {
Logger.notice(this, "Invalid signature !");
wotPublicKeySignature = null;
wotPublicKey = null;
return invalidMessage("Invalid signature");
}
if (wotPublicKey != null) {
if (!checkWotPublicKeySignature(identity, wotPublicKey, wotPublicKeySignature)) {
wotPublicKeySignature = null;
wotPublicKey = null;
}
}
return true;
}
protected boolean loadXMLElements(Element root) {
messageId = XMLTools.getChildElementsCDATAValue(root, "MessageId");
inReplyTo = XMLTools.getChildElementsCDATAValue(root, "InReplyTo");
inReplyToFull = inReplyTo;
from = XMLTools.getChildElementsCDATAValue(root, "From");
subject = XMLTools.getChildElementsCDATAValue(root, "Subject");
date = XMLTools.getChildElementsCDATAValue(root, "Date");
time = XMLTools.getChildElementsCDATAValue(root, "Time");
if (time == null) time = "00:00:00GMT";
time = time.replaceAll("GMT", "");
recipient = XMLTools.getChildElementsCDATAValue(root, "recipient");
board = XMLTools.getChildElementsCDATAValue(root, "Board");
if (board == null) board = ""; /* won't validate a check in insert() :p */
body = XMLTools.getChildElementsCDATAValue(root, "Body");
signature = XMLTools.getChildElementsCDATAValue(root, "SignatureV2");
publicKey = XMLTools.getChildElementsCDATAValue(root, "pubKey");
idLinePos = XMLTools.getChildElementsTextValue(root, "IdLinePos");
idLineLen = XMLTools.getChildElementsTextValue(root, "IdLineLen");
wotPublicKey = XMLTools.getChildElementsTextValue(root, "trustListPublicKey");
wotPublicKeySignature = XMLTools.getChildElementsTextValue(root, "trustListPublicKeySignature");
List l = XMLTools.getChildElementsByTagName(root, "AttachmentList");
if (l.size() == 1) {
attachments = new Vector();
KSKAttachmentFactory factory = new KSKAttachmentFactory();
Element attachmentsEl = (Element) l.get(0);
Iterator i = XMLTools.getChildElementsByTagName(attachmentsEl,"Attachment").iterator();
while (i.hasNext()){
Element el = (Element)i.next();
KSKAttachment attachment = factory.getAttachment(el);
if (attachment != null)
attachments.add(attachment);
}
}
if (from == null || subject == null || body == null) {
Logger.notice(this, "Field missing");
return false;
}
return true;
}
protected boolean decrypt(Hsqldb db, Element rootNode) {
Vector identities = Identity.getYourIdentities(db);
byte[] content;
String recipient = XMLTools.getChildElementsCDATAValue(rootNode, "recipient");
try {
content = Base64.decode(XMLTools.getChildElementsCDATAValue(rootNode,
"content").getBytes("UTF-8")); /* ISO-8859-1 in Frost */
} catch(Exception e) {
Logger.notice(this, "Unable to decode encrypted message because : "+e.toString());
return false;
}
Identity identity = null;
for (Iterator it = identities.iterator();
it.hasNext();) {
Identity id = (Identity)it.next();
if (id.toString().equals(recipient)) {
identity = id;
break;
}
}
if (identity == null) {
Logger.info(this, "Not for us but for '"+recipient+"'");
}
byte[] decoded = null;
if (identity != null)
decoded = identity.decode(content);
if (decoded != null) {
/*** we are able to decrypt it ***/
/* Hm, there should be a better way (all in RAM) */
encryptedFor = identity;
File tmp = null;
boolean ret = false;
try {
tmp = File.createTempFile("thaw-", "-decrypted-msg.xml");
tmp.deleteOnExit();
frost.util.FileAccess.writeFile(decoded, tmp);
/* recursivity (bad bad bad, but I'm lazy :) */
ret = loadFile(tmp, db);
} catch(Exception e) {
Logger.warning(this, "Unable to read the decrypted message because: "+e.toString());
}
if (tmp != null)
tmp.delete();
return ret;
}
/*** Unable to decrypt the message, but we will store what we know anyway ***
* to not fetch this message again
*/
/* (I'm still thinking that mixing up Boards & private messages is a BAD idea) */
inReplyTo = null;
inReplyToFull = null;
from = "["+I18n.getMessage("thaw.plugin.miniFrost.encrypted")+"]";
subject = "["+I18n.getMessage("thaw.plugin.miniFrost.encryptedBody").replaceAll("X", recipient)+"]";
String[] date = gmtFormat.format(new Date()).toString().split(" ");
this.date = date[0];
this.time = date[1];
/* not really used */
this.recipient = recipient;
/* will be ignored by the checks */
this.board = null;
this.body = I18n.getMessage("thaw.plugin.miniFrost.encryptedBody").replaceAll("X", recipient);
this.publicKey = null;
this.signature = null;
this.idLinePos = "0";
this.idLineLen = "0";
attachments = null;
identity = null;
if (frostCrypt == null)
frostCrypt = new FrostCrypt();
this.messageId = frostCrypt.computeChecksumSHA256(date[0] + date[1]);
read = true;
archived = true;
/* because we have date to store: */
return true;
}
private boolean invalidMessage(String reason) {
/* Invalid message -> Will use default value */
/* the goal is to not download this message again */
inReplyTo = null;
inReplyToFull = null;
from = "["+I18n.getMessage("thaw.plugin.miniFrost.invalidMessage")+"]";
subject = "["+I18n.getMessage("thaw.plugin.miniFrost.invalidMessage")+"]";
String[] date = gmtFormat.format(new Date()).toString().split(" ");
this.date = date[0];
this.time = date[1];
/* not really used */
this.recipient = null;
/* will be ignored by the checks */
this.board = null;
this.body = I18n.getMessage("thaw.plugin.miniFrost.invalidMessage")+
((reason != null) ? "\n"+reason : "");
this.publicKey = null;
this.signature = null;
this.idLinePos = "0";
this.idLineLen = "0";
attachments = null;
identity = null;
if (frostCrypt == null)
frostCrypt = new FrostCrypt();
this.messageId = frostCrypt.computeChecksumSHA256(date[0] + date[1]);
read = true;
archived = true;
return true;
}
/**
* This function has been imported from FROST.
* Parses the XML file and passes the FrostMessage element to XMLize load method.
* @param db require if the message is encrypted
*/
public boolean loadFile(File file, Hsqldb db) {
try {
Document doc = null;
try {
doc = XMLTools.parseXmlFile(file, false);
} catch(Exception ex) { // xml format error
Logger.notice(this, "Invalid Xml");
loaded = false;
//return invalidMessage("XML parser error:\n"+ex.toString());
return false;
}
if( doc == null ) {
Logger.notice(this,
"Error: couldn't parse XML Document - " +
"File name: '" + file.getName() + "'");
loaded = false;
//return invalidMessage("Nothing parsed ?!");
return false;
}
Element rootNode = doc.getDocumentElement();
if(rootNode.getTagName().equals("EncryptedFrostMessage")) {
if (db != null && decrypt(db, rootNode)) {
loaded = true;
return true;
} else {
loaded = false;
Logger.error(this, "Can't decrypt the message");
return false;
}
}
// load the message itself
loaded = (loadXMLElements(rootNode));
return loaded;
} catch(Exception e) {
/* XMLTools throws runtime exception sometimes ... */
Logger.notice(this, "Unable to parse XML message because : "+e.toString());
e.printStackTrace();
loaded = false;
//return invalidMessage("Unable to parse XML message because : "+e.toString());
return false;
}
}
public Element makeText(Document doc, String tagName, String content) {
if (content == null || tagName == null)
return null;
Text txt;
Element current;
current = doc.createElement(tagName);
txt = doc.createTextNode(content);
current.appendChild(txt);
return current;
}
public Element makeCDATA(Document doc, String tagName, String content) {
if (content == null || tagName == null)
return null;
CDATASection cdata;
Element current;
current = doc.createElement(tagName);
cdata = doc.createCDATASection(content);
current.appendChild(cdata);
return current;
}
public String getFullInReplyTo(Hsqldb db, String inReplyTo) {
String lastId = inReplyTo;
PreparedStatement st;
synchronized(db.dbLock) {
try {
st = db.getConnection().prepareStatement("SELECT inReplyToId FROM frostKSKMessages "+
"WHERE msgId = ? LIMIT 1");
} catch(SQLException e) {
Logger.error(this, "Can't get full inReplyTo String because: "+e.toString());
return inReplyTo;
}
while(lastId != null) {
/* I don't remember if inReplyTo is correctly set, so we will
* use inReplyToId to be safer
*/
try {
st.setString(1, lastId);
ResultSet set = st.executeQuery();
if (set.next()) {
lastId = set.getString("inReplyToId");
if (lastId != null)
inReplyTo = lastId + ","+inReplyTo;
} else
lastId = null;
} catch(SQLException e) {
Logger.error(this, "Can't find message parent because : "+e.toString());
lastId = null;
}
}
try {
st.close();
} catch(SQLException e) {
/* \_o< */
}
}
return inReplyTo;
}
public Element getXMLTree(Hsqldb db, Document doc) {
Element root = doc.createElement("FrostMessage");
Element el;
if ((el = makeText( doc, "client", "Thaw "+thaw.core.Main.VERSION)) != null)
root.appendChild(el);
if (wotPublicKey != null) {
if ((el = makeText(doc, "trustListPublicKey", wotPublicKey)) != null)
root.appendChild(el);
if ((el = makeText(doc, "trustListPublicKeySignature", wotPublicKeySignature)) != null)
root.appendChild(el);
}
if ((el = makeCDATA(doc, "MessageId", messageId)) != null) root.appendChild(el);
if ((el = makeCDATA(doc, "InReplyTo", inReplyToFull)) != null) root.appendChild(el);
if ((el = makeText( doc, "IdLinePos", idLinePos)) != null) root.appendChild(el);
if ((el = makeText( doc, "IdLineLen", idLineLen)) != null) root.appendChild(el);
if ((el = makeCDATA(doc, "From", from)) != null) root.appendChild(el);
if ((el = makeCDATA(doc, "Subject", subject)) != null) root.appendChild(el);
if ((el = makeCDATA(doc, "Date", date)) != null) root.appendChild(el);
if ((el = makeCDATA(doc, "Time", time+"GMT")) != null) root.appendChild(el);
if ((el = makeCDATA(doc, "Body", body)) != null) root.appendChild(el);
if ((el = makeCDATA(doc, "Board", board)) != null) root.appendChild(el);
if ((el = makeCDATA(doc, "pubKey", publicKey)) != null) root.appendChild(el);
if ((el = makeCDATA(doc, "recipient", recipient)) != null) root.appendChild(el);
if ((el = makeCDATA(doc, "SignatureV2", signature)) != null) root.appendChild(el);
if (attachments != null) {
el = doc.createElement("AttachmentList");
for (Iterator it = attachments.iterator();
it.hasNext();) {
el.appendChild(((KSKAttachment)it.next()).getXML(doc));
}
root.appendChild(el);
}
return root;
}
private byte[] readByteArray(File file) {
try {
byte[] data = new byte[(int)file.length()];
FileInputStream fileIn = new FileInputStream(file);
DataInputStream din = new DataInputStream(fileIn);
din.readFully(data);
fileIn.close();
return data;
} catch(java.io.IOException e) {
Logger.error(this, "Exception thrown in readByteArray(File file): '"+e.toString()+"'");
}
return null;
}
public File crypt(Identity receiver, File msgFile) {
File tmpFile;
try {
tmpFile = File.createTempFile("thaw-", "-message.xml");
tmpFile.deleteOnExit();
} catch(java.io.IOException e) {
Logger.error(this, "Can't create temporary file because : "+e.toString());
return null;
}
Document doc = XMLTools.createDomDocument();
Element el;
Element root = doc.createElement("EncryptedFrostMessage");
/* first tag : recipient */
if ((el = makeCDATA(doc, "recipient", receiver.toString())) != null) root.appendChild(el);
/* second tag : crypted content */
byte[] xmlContent = readByteArray(msgFile);
byte[] encContent = receiver.encode(xmlContent);
String base64enc;
try {
base64enc = new String(Base64.encode(encContent), "UTF-8"); /* ISO-8859-1 in Frost */
} catch (UnsupportedEncodingException ex) {
Logger.error(this, "UTF-8 encoding is not supported ?! : '"+ex.toString()+"'");
return null;
}
if ((el = makeCDATA(doc, "content", base64enc)) != null) root.appendChild(el);
doc.appendChild(root);
File cryptedFile = (XMLTools.writeXmlFile(doc, tmpFile.getPath()) ? tmpFile : null);
return cryptedFile;
}
public File generateXML(Hsqldb db) {
File tmpFile;
try {
tmpFile = File.createTempFile("thaw-", "-message.xml");
tmpFile.deleteOnExit();
} catch(java.io.IOException e) {
Logger.error(this, "Can't create temporary file because : "+e.toString());
return null;
}
Document doc = XMLTools.createDomDocument();
doc.appendChild(getXMLTree(db, doc));
File clearMsg = (XMLTools.writeXmlFile(doc, tmpFile.getPath()) ? tmpFile : null);
if (encryptedFor == null)
return clearMsg;
File cryptedFile = crypt(encryptedFor, clearMsg);
tmpFile.delete();
return cryptedFile;
}
protected boolean mustBeDisplayedAsRead() {
return read;
}
}