/*
* Copyright 2013 mpowers
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.trsst.client;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import javax.activation.MimeType;
import javax.xml.namespace.QName;
import org.apache.abdera.Abdera;
import org.apache.abdera.i18n.iri.IRI;
import org.apache.abdera.model.Content;
import org.apache.abdera.model.Document;
import org.apache.abdera.model.Element;
import org.apache.abdera.model.Entry;
import org.apache.abdera.model.Feed;
import org.apache.abdera.model.Link;
import org.apache.abdera.model.Person;
import org.apache.abdera.protocol.Response.ResponseType;
import org.apache.abdera.protocol.client.AbderaClient;
import org.apache.abdera.protocol.client.ClientResponse;
import org.apache.abdera.security.AbderaSecurity;
import org.apache.abdera.security.SecurityException;
import org.apache.abdera.security.Signature;
import org.apache.abdera.security.SignatureOptions;
import org.apache.abdera.writer.StreamWriter;
import org.apache.commons.codec.binary.Base64;
import com.trsst.Common;
import com.trsst.Crypto;
/**
* Implements the protocol-level features of the Trsst platform: creating Feeds
* and Entries, signing them, and optionally encrypting Entries when desired.
*
* @author mpowers
*/
public class Client {
private URL serving;
/**
* Client connecting to trsst services hosted at the specified url.
*/
public Client(URL url) {
this.serving = url;
}
/**
* Returns the url this client is using to connect to services.
*/
public URL getURL() {
return this.serving;
}
/**
* Returns a Feed for the specified feed id, and will attempt to decrypt any
* encrypted content with the specified key.
*
* @param urn
* a feed or entry urn id.
* @param decryptionKey
* one or more private keys used to attempt to decrypt content.
* @return a Feed containing the latest entries for this feed id.
*/
public Feed pull(String urn, PrivateKey[] decryptionKeys) {
Feed feed = pull(urn);
if ( feed == null ) {
return null;
}
Content content;
MimeType contentType;
for (Entry entry : feed.getEntries()) {
content = entry.getContentElement();
if (content != null
&& (contentType = content.getMimeType()) != null
&& "application/xenc+xml".equals(contentType.toString())) {
// if this message was intended for us, we will be able to
// decrypt one of the elements into an AES key to decrypt the
// encrypted entry itself
QName publicEncryptName = new QName(Common.NS_URI,
Common.ENCRYPT);
QName publicSignName = new QName(Common.NS_URI, Common.SIGN);
QName encryptedDataName = new QName(
"http://www.w3.org/2001/04/xmlenc#", "EncryptedData");
QName cipherDataName = new QName(
"http://www.w3.org/2001/04/xmlenc#", "CipherData");
QName cipherValueName = new QName(
"http://www.w3.org/2001/04/xmlenc#", "CipherValue");
String encodedBytes;
byte[] decodedBytes;
Element publicKeyElement, cipherData, cipherValue, result;
List<Element> encryptedElements = content.getElements();
int lastIndex = encryptedElements.size() - 1;
Element element;
PublicKey publicKey = null;
byte[] decryptedKey = null;
publicKeyElement = feed.getFirstChild(publicEncryptName);
if (publicKeyElement == null) {
// fall back on signing key
publicKeyElement = feed.getFirstChild(publicSignName);
}
if (publicKeyElement != null
&& publicKeyElement.getText() != null) {
try {
publicKey = Common.toPublicKeyFromX509(publicKeyElement
.getText());
} catch (GeneralSecurityException gse) {
log.error("Could not parse public key: "
+ publicKeyElement);
}
}
if (publicKey != null) {
// TODO: if we're the author, we can start loop at
// (lastIndex-1)
for (int i = 0; i < encryptedElements.size(); i++) {
element = encryptedElements.get(i);
if (encryptedDataName.equals(element.getQName())) {
cipherData = element.getFirstChild(cipherDataName);
if (cipherData != null) {
cipherValue = cipherData
.getFirstChild(cipherValueName);
if (cipherValue != null) {
encodedBytes = cipherValue.getText();
if (encodedBytes != null) {
decodedBytes = new Base64()
.decode(encodedBytes);
if (i != lastIndex) {
// if we're not at the last index
// (the payload) so we should
// attempt
// to decrypt this AES key
for (PrivateKey decryptionKey : decryptionKeys) {
try {
decryptedKey = Crypto
.decryptKeyWithIES(
decodedBytes,
entry.getUpdated()
.getTime(),
publicKey,
decryptionKey);
if (decryptedKey != null) {
// success:
// skip to lastIndex
i = lastIndex - 1;
break;
}
} catch (GeneralSecurityException e) {
// key did not fit
log.trace(
"Could not decrypt key: "
+ entry.getId(),
e);
} catch (Throwable t) {
log.warn(
"Error while decrypting key on entry: "
+ entry.getId(),
t);
}
}
} else if (decryptedKey != null) {
// if we're at the last index
// (the payload) and we have an
// AES key: attempt to decrypt
try {
result = decryptElementAES(
decodedBytes,
decryptedKey);
for (Element ee : encryptedElements) {
ee.discard();
}
content.setValueElement(result);
break;
} catch (SecurityException e) {
log.error(
"Key did not decrypt element: "
+ entry.getId(),
e);
} catch (Throwable t) {
log.warn(
"Could not decrypt element on entry: "
+ entry.getId(),
t);
}
}
} else {
log.warn("No cipher text for entry: "
+ entry.getId());
}
} else {
log.warn("No cipher value for entry: "
+ entry.getId());
}
} else {
log.warn("No cipher data for entry: "
+ entry.getId());
}
}
}
} else {
log.error("No public key for feed: " + feed.getId());
}
}
}
return feed;
}
/**
* Returns a Feed for the specified urn. Filters may be applied as url
* parameters on the urn, e.g. "?tag=birthday&tag=happy"
*
* @param urn
* a feed or entry urn id.
* @return a Feed containing the latest entries for this feed id.
*/
public Feed pull(String urn) {
AbderaClient client = new AbderaClient(Abdera.getInstance(),
Common.getBuildString());
if (urn.startsWith("urn:feed:")) {
urn = urn.substring("urn:feed:".length());
} else if (urn.startsWith("urn:entry:")) {
urn = urn.substring("urn:entry:".length());
// convert from urn to a trsst url path
int sep = urn.lastIndexOf(':');
if (sep != -1) {
urn = urn.substring(0, sep) + '/' + urn.substring(sep + 1);
}
}
URL url = null;
try {
url = new URL(serving + "/" + urn);
} catch (MalformedURLException e) {
System.err.println("Invalid urn: " + serving + "/" + urn);
return null;
}
ClientResponse response = client.get(url.toString());
if (response.getType() == ResponseType.SUCCESS) {
Document<Feed> document = response.getDocument();
if (document != null) {
return document.getRoot();
} else {
log.warn("pull: no document for: " + url);
}
} else {
log.debug("pull: no document found for: " + url + " : "
+ response.getType());
}
return null;
}
/**
* Pushes entries from the specified feed id on the home service to the
* remote services hosted at the specified URL.
*
* Push is used to notify a remote service of new entries that may be of
* interest to users whose home is that service, or more broadly to publish
* and propagate content across the network of participating trsst servers.
*
* @param feedId
* a feed id.
* @param url
* a URL to a remote trsst service
* @return a Feed returned by the server successfully accepting the feed, or
* null if unsuccessful.
*/
public Feed push(String feedId, URL url) {
return push(pull(feedId), url);
}
private Feed push(Feed feed, String[] contentId, String[] contentType,
byte[][] content, URL url) {
try {
AbderaClient client = new AbderaClient(Abdera.getInstance());
url = new URL(url.toString() + '/'
+ Common.fromFeedUrn(feed.getId()));
ClientResponse response;
if (contentId != null) {
response = client.post(url.toString(),
new MultiPartRequestEntity(feed, content, contentId,
contentType));
} else {
response = client.post(url.toString(), feed);
}
if (response.getType() == ResponseType.SUCCESS) {
Document<Feed> document = response.getDocument();
if (document != null) {
return document.getRoot();
} else {
log.warn("push: no document for: " + url);
}
} else {
System.err.println("Sent:");
System.err.println(feed);
log.error("push: invalid response for: " + url + " : "
+ response.getType());
System.err.println("Received:");
System.err.println(response.getDocument().getRoot());
throw new IllegalArgumentException(response.getDocument()
.getRoot().toString());
}
} catch (MalformedURLException e) {
log.error("push: bad url: " + url, e);
}
return null;
}
/**
* Pushes entries from the specified feed to the remote services hosted at
* the specified URL.
*
* @param feed
* a feed containing entries
* @param url
* a URL to a remote trsst service
* @return a Feed returned by the server successfully accepting the feed, or
* null if unsuccessful.
*/
public Feed push(Feed feed, URL url) {
return push(feed, null, null, null, url);
}
/**
* Posts a new entry to the feed associated with the specified public
* signing key to the home server, creating a new feed if needed.
*
* @param signingKeys
* Required: the signing keys associated with public feed id of
* this feed.
* @param encryptionKey
* Required: the public encryption key associated with this
* account; this public key will be used by others to encrypt
* private message for this account.
* @param options
* The data to be posted.
* @return The feed as posted to the home server.
* @throws IOException
* @throws SecurityException
* @throws GeneralSecurityException
* @throws contentKey
*/
public Feed post(KeyPair signingKeys, KeyPair encryptionKeys,
EntryOptions options, FeedOptions feedOptions) throws IOException,
SecurityException, GeneralSecurityException, Exception {
// inlining all the steps to help implementors and porters (and
// 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"));
if (keyInfo != null) {
// remove key info (because we're not using certs)
keyInfo.discard();
}
feed.addExtension(signatureElement);
// add the new entry to the feed, if there is one,
// only after we have signed the feed
if (entry != null) {
feed.addEntry(entry);
}
// post to server
if (contentIds.length > 0) {
return push(feed, contentIds, options.getMimetypes(),
options.getContentData(), serving);
}
return push(feed, serving);
}
private final static SignatureOptions getSignatureOptions(Signature signer,
KeyPair signingKeys) throws SecurityException {
SignatureOptions options = signer.getDefaultSignatureOptions();
options.setSigningAlgorithm("http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1");
options.setSignLinks(false); // don't sign atom:links
options.setPublicKey(signingKeys.getPublic());
options.setSigningKey(signingKeys.getPrivate());
return options;
}
public static byte[] encryptElementAES(Element element, byte[] secretKey)
throws SecurityException {
byte[] after = null;
try {
ByteArrayOutputStream output = new ByteArrayOutputStream();
element.writeTo(output);
byte[] before = output.toByteArray();
after = Crypto.encryptAES(before, secretKey);
} catch (Exception e) {
log.error("Error while encrypting element", e);
throw new SecurityException(e);
}
return after;
}
public static Element decryptElementAES(byte[] data, byte[] secretKey)
throws SecurityException {
Element result;
try {
byte[] after = Crypto.decryptAES(data, secretKey);
ByteArrayInputStream input = new ByteArrayInputStream(after);
result = Abdera.getInstance().getParser().parse(input).getRoot();
} catch (Exception e) {
log.error("Error while decrypting: ", e);
throw new SecurityException(e);
}
return result;
}
private final static org.slf4j.Logger log = org.slf4j.LoggerFactory
.getLogger(Client.class);
}