// debuggers)
// configure for signing
Element signedNode, signatureElement, keyInfo;
AbderaSecurity security = new AbderaSecurity(Abdera.getInstance());
Signature signer = security.getSignature();
String feedId = Common.toFeedId(signingKeys.getPublic());
Feed feed = pull(feedId);
if (feed == null) {
feed = Abdera.getInstance().newFeed();
feed.declareNS(Common.NS_URI, Common.NS_ABBR);
}
// remove each entry and retain the most recent one (if any)
List<Entry> entries = feed.getEntries();
Entry mostRecentEntry = null;
for (Entry entry : entries) {
if (mostRecentEntry == null || mostRecentEntry.getUpdated() == null
|| mostRecentEntry.getUpdated().before(entry.getUpdated())) {
mostRecentEntry = entry;
}
entry.discard();
}
// update and sign feed (without any entries)
feed.setUpdated(new Date());
// ensure the correct keys are in place
signatureElement = feed.getFirstChild(new QName(Common.NS_URI,
Common.SIGN));
if (signatureElement != null) {
signatureElement.discard();
}
feed.addExtension(new QName(Common.NS_URI, Common.SIGN)).setText(
Common.toX509FromPublicKey(signingKeys.getPublic()));
signatureElement = feed.getFirstChild(new QName(Common.NS_URI,
Common.ENCRYPT));
if (signatureElement != null) {
signatureElement.discard();
}
feed.addExtension(new QName(Common.NS_URI, Common.ENCRYPT)).setText(
Common.toX509FromPublicKey(encryptionKeys.getPublic()));
feed.setId(Common.toFeedUrn(feedId));
feed.setMustPreserveWhitespace(false);
// update feed properties
if (feedOptions.title != null) {
feed.setTitle(feedOptions.title);
}
if (feedOptions.subtitle != null) {
feed.setSubtitle(feedOptions.subtitle);
}
if (feedOptions.icon != null) {
while (feed.getIconElement() != null) {
feed.getIconElement().discard();
}
feed.setIcon(feedOptions.icon);
}
if (feedOptions.logo != null) {
while (feed.getLogoElement() != null) {
feed.getLogoElement().discard();
}
feed.setLogo(feedOptions.logo);
}
Person author = feed.getAuthor();
if (author == null) {
// author is a required element
author = Abdera.getInstance().getFactory().newAuthor();
String defaultName = feed.getTitle();
if (defaultName == null) {
defaultName = Common.toFeedIdString(feed.getId());
}
author.setName(defaultName);
feed.addAuthor(author);
}
// update author
if (feedOptions.name != null || feedOptions.email != null
|| feedOptions.uri != null) {
if (feedOptions.name != null) {
author.setName(feedOptions.name);
}
if (feedOptions.email != null) {
author.setEmail(feedOptions.email);
}
if (feedOptions.uri != null) {
if (feedOptions.uri.indexOf(':') == -1) {
// default to "acct:" urn
author.setUri("acct:" + feedOptions.uri + ".trsst.com");
// FIXME: domain should be specified by user
} else {
author.setUri(feedOptions.uri);
}
}
}
// set base
if (feedOptions.base != null) {
String uri = feedOptions.base;
if (!uri.endsWith("/")) {
uri = uri + '/';
}
uri = uri + feedId;
feed.setBaseUri(uri);
}
// set link self
IRI base = feed.getBaseUri();
if (base != null) {
while (feed.getLink(Link.REL_SELF) != null) {
feed.getLink(Link.REL_SELF).discard();
}
feed.addLink(base.toString(), Link.REL_SELF);
}
// holds any attachments (can be used for logo and icons)
String[] contentIds = new String[options.getContentCount()];
// subject or verb or attachment is required to create an entry
Entry entry = null;
if (options.status != null || options.verb != null
|| contentIds.length > 0) {
// create the new entry
entry = Abdera.getInstance().newEntry();
entry.setUpdated(feed.getUpdated());
entry.setId(Common.toEntryUrn(feedId, feed.getUpdated().getTime()));
entry.addLink(feedId + '/' + Common.toEntryIdString(entry.getId()));
if (options.publish != null) {
entry.setPublished(options.publish);
} else {
entry.setPublished(entry.getUpdated());
}
if (options.status != null) {
entry.setTitle(options.status);
} else {
// title is a required element:
// default to verb
if (options.verb != null) {
entry.setTitle(options.verb);
} else {
// "post" is the default verb
entry.setSummary("post");
}
}
if (options.verb != null) {
feed.declareNS("http://activitystrea.ms/spec/1.0/", "activity");
entry.addSimpleExtension(
new QName("http://activitystrea.ms/spec/1.0/", "verb",
"activity"), options.verb);
}
if (options.body != null) {
// was: entry.setSummary(options.body);
entry.setSummary(options.body,
org.apache.abdera.model.Text.Type.HTML);
// FIXME: some readers only show type=html
} else {
// summary is a required element in some cases
entry.setSummary("", org.apache.abdera.model.Text.Type.TEXT);
// FIXME: use tika to generate a summary
}
// generate proof-of-work stamp for this feed id and entry id
Element stampElement = entry.addExtension(new QName(Common.NS_URI,
Common.STAMP));
stampElement.setText(Crypto.computeStamp(Common.STAMP_BITS, entry
.getUpdated().getTime(), feedId));
if (options.mentions != null) {
HashSet<String> set = new HashSet<String>();
for (String s : options.mentions) {
if (!set.contains(s)) {
set.add(s); // prevent duplicates
entry.addCategory(Common.MENTION_URN, s, "Mention");
stampElement = entry.addExtension(new QName(
Common.NS_URI, Common.STAMP));
stampElement.setText(Crypto.computeStamp(
Common.STAMP_BITS,
entry.getUpdated().getTime(), s));
// stamp is required for each mention
}
}
}
if (options.tags != null) {
HashSet<String> set = new HashSet<String>();
for (String s : options.tags) {
if (!set.contains(s)) {
set.add(s); // prevent duplicates
entry.addCategory(Common.TAG_URN, s, "Tag");
stampElement = entry.addExtension(new QName(
Common.NS_URI, Common.STAMP));
stampElement.setText(Crypto.computeStamp(
Common.STAMP_BITS,
entry.getUpdated().getTime(), s));
// stamp is required for each tag
}
}
}
// generate an AES256 key for encrypting
byte[] contentKey = null;
if (options.recipientIds != null) {
contentKey = Crypto.generateAESKey();
}
// for each content part
for (int part = 0; part < contentIds.length; part++) {
byte[] currentContent = options.getContentData()[part];
String currentType = options.getMimetypes()[part];
// encrypt before hashing if necessary
if (contentKey != null) {
currentContent = Crypto.encryptAES(currentContent,
contentKey);
}
// calculate digest to determine content id
byte[] digest = Common.ripemd160(currentContent);
contentIds[part] = new Base64(0, null, true)
.encodeToString(digest);
// add mime-type hint to content id (if not encrypted):
// (some readers like to see a file extension on enclosures)
if (currentType != null && contentKey == null) {
String extension = "";
int i = currentType.lastIndexOf('/');
if (i != -1) {
extension = '.' + currentType.substring(i + 1);
}
contentIds[part] = contentIds[part] + extension;
}
// set the content element
if (entry.getContentSrc() == null) {
// only point to the first attachment if multiple
entry.setContent(new IRI(contentIds[part]), currentType);
}
// use a base uri so src attribute is simpler to process
entry.getContentElement().setBaseUri(
Common.toEntryIdString(entry.getId()) + '/');
entry.getContentElement().setAttributeValue(
new QName(Common.NS_URI, "hash", "trsst"), "ripemd160");
// if not encrypted
if (contentKey == null) {
// add an enclosure link
entry.addLink(Common.toEntryIdString(entry.getId()) + '/'
+ contentIds[part], Link.REL_ENCLOSURE,
currentType, null, null, currentContent.length);
}
}
if (contentIds.length == 0 && options.url != null) {
Content content = Abdera.getInstance().getFactory()
.newContent();
if (options.url.startsWith("urn:feed:")
|| options.url.startsWith("urn:entry:")) {
content.setMimeType("application/atom+xml");
} else {
content.setMimeType("text/html");
}
content.setSrc(options.url);
entry.setContentElement(content);
}
// add the previous entry's signature value
String predecessor = null;
if (mostRecentEntry != null) {
signatureElement = mostRecentEntry.getFirstChild(new QName(
"http://www.w3.org/2000/09/xmldsig#", "Signature"));
if (signatureElement != null) {
signatureElement = signatureElement
.getFirstChild(new QName(
"http://www.w3.org/2000/09/xmldsig#",
"SignatureValue"));
if (signatureElement != null) {
predecessor = signatureElement.getText();
signatureElement = entry.addExtension(new QName(
Common.NS_URI, Common.PREDECESSOR));
signatureElement.setText(predecessor);
signatureElement.setAttributeValue(
Common.PREDECESSOR_ID, mostRecentEntry.getId()
.toString());
} else {
log.error("No signature value found for entry: "
+ entry.getId());
}
} else {
log.error("No signature found for entry: " + entry.getId());
}
}
if (options.recipientIds == null) {
// public post
entry.setRights(Common.RIGHTS_NDBY_REVOCABLE);
} else {
// private post
entry.setRights(Common.RIGHTS_RESERVED);
try {
StringWriter stringWriter = new StringWriter();
StreamWriter writer = Abdera.getInstance()
.getWriterFactory().newStreamWriter();
writer.setWriter(stringWriter);
writer.startEntry();
writer.writeId(entry.getId());
writer.writeUpdated(entry.getUpdated());
writer.writePublished(entry.getPublished());
if (predecessor != null) {
writer.startElement(Common.PREDECESSOR, Common.NS_URI);
writer.writeElementText(predecessor);
writer.endElement();
}
if (options.publicOptions != null) {
// these are options that will be publicly visible
if (options.publicOptions.status != null) {
writer.writeTitle(options.publicOptions.status);
} else {
writer.writeTitle(""); // empty title
}
if (options.publicOptions.body != null) {
writer.writeSummary(options.publicOptions.body);
}
if (options.publicOptions.verb != null) {
writer.startElement("verb",
"http://activitystrea.ms/spec/1.0/");
writer.writeElementText(options.publicOptions.verb);
writer.endElement();
}
if (options.publicOptions.tags != null) {
for (String s : options.publicOptions.tags) {
writer.writeCategory(s);
}
}
if (options.publicOptions.mentions != null) {
for (String s : options.publicOptions.mentions) {
writer.startElement("mention", Common.NS_URI,
"trsst");
writer.writeElementText(s);
writer.endElement();
}
}
} else {
writer.writeTitle(""); // empty title
}
writer.startContent("application/xenc+xml");
List<PublicKey> keys = new LinkedList<PublicKey>();
for (String id : options.recipientIds) {
// for each recipient
Feed recipientFeed = pull(id);
if (recipientFeed != null) {
// fetch encryption key
Element e = recipientFeed.getExtension(new QName(
Common.NS_URI, Common.ENCRYPT));
if (e == null) {
// fall back to signing key
e = recipientFeed.getExtension(new QName(
Common.NS_URI, Common.SIGN));
}
keys.add(Common.toPublicKeyFromX509(e.getText()));
}
}
// enforce the convention:
keys.remove(encryptionKeys.getPublic());
// move to end if exists;
// last encrypted key is for ourself
keys.add(encryptionKeys.getPublic());
// encrypt content key separately for each recipient
for (PublicKey recipient : keys) {
byte[] bytes = Crypto.encryptKeyWithIES(contentKey,
feed.getUpdated().getTime(), recipient,
encryptionKeys.getPrivate());
String encoded = new Base64(0, null, true)
.encodeToString(bytes);
writer.startElement("EncryptedData",
"http://www.w3.org/2001/04/xmlenc#");
writer.startElement("CipherData",
"http://www.w3.org/2001/04/xmlenc#");
writer.startElement("CipherValue",
"http://www.w3.org/2001/04/xmlenc#");
writer.writeElementText(encoded);
writer.endElement();
writer.endElement();
writer.endElement();
}
// now: encrypt the payload with content key
byte[] bytes = encryptElementAES(entry, contentKey);
String encoded = new Base64(0, null, true)
.encodeToString(bytes);
writer.startElement("EncryptedData",
"http://www.w3.org/2001/04/xmlenc#");
writer.startElement("CipherData",
"http://www.w3.org/2001/04/xmlenc#");
writer.startElement("CipherValue",
"http://www.w3.org/2001/04/xmlenc#");
writer.writeElementText(encoded);
writer.endElement();
writer.endElement();
writer.endElement();
// done with encrypted elements
writer.endContent();
writer.endEntry();
writer.flush();
// this constructed entry now replaces the encrypted
// entry
entry = (Entry) Abdera.getInstance().getParserFactory()
.getParser()
.parse(new StringReader(stringWriter.toString()))
.getRoot();
// System.out.println(stringWriter.toString());
} catch (Throwable t) {
log.error("Unexpected error while encrypting, exiting: "
+ options.recipientIds, t);
t.printStackTrace();
throw new IllegalArgumentException("Unexpected error: " + t);
}
}
// sign the new entry
signedNode = signer.sign(entry,
getSignatureOptions(signer, signingKeys));
signatureElement = signedNode.getFirstChild(new QName(
"http://www.w3.org/2000/09/xmldsig#", "Signature"));
keyInfo = signatureElement.getFirstChild(new QName(
"http://www.w3.org/2000/09/xmldsig#", "KeyInfo"));
if (keyInfo != null) {
// remove key info (because we're not using certs)
keyInfo.discard();
}
entry.addExtension(signatureElement);
} else {
log.info("No valid entries detected; updating feed.");
}
// remove existing feed signature element if any
signatureElement = feed.getFirstChild(new QName(
"http://www.w3.org/2000/09/xmldsig#", "Signature"));
if (signatureElement != null) {
signatureElement.discard();
}
// remove all navigation links before signing
for (Link link : feed.getLinks()) {
if (Link.REL_FIRST.equals(link.getRel())
|| Link.REL_LAST.equals(link.getRel())
|| Link.REL_CURRENT.equals(link.getRel())
|| Link.REL_NEXT.equals(link.getRel())
|| Link.REL_PREVIOUS.equals(link.getRel())) {
link.discard();
}
}
// remove all opensearch elements before signing
for (Element e : feed
.getExtensions("http://a9.com/-/spec/opensearch/1.1/")) {
e.discard();
}
// set logo and/or icon
if (contentIds.length > 0) {
String url = Common.toEntryIdString(entry.getId()) + '/'
+ contentIds[0];
if (feedOptions.asIcon) {
feed.setIcon(url);
}
if (feedOptions.asLogo) {
feed.setLogo(url);
}
}
// sign the feed
signedNode = signer
.sign(feed, getSignatureOptions(signer, signingKeys));
signatureElement = signedNode.getFirstChild(new QName(
"http://www.w3.org/2000/09/xmldsig#", "Signature"));
keyInfo = signatureElement.getFirstChild(new QName(
"http://www.w3.org/2000/09/xmldsig#", "KeyInfo"));