Skip to content
Thomas Schwotzer edited this page Jan 5, 2022 · 21 revisions

Implementation

It is helpful and effectiv to think of software as layered architecture. Each implementation is a layer. Each implementation bridges a gap. There is an API on one side. It provides services to layer above. That API describes features. It was created to implement our use cases. This API will be used e.g. by a (graphical) user interface.

On the other side, each implementation is based on other software the layer below. That is the other side. In our case, that software below is ASAPJava and SharkPKI.

Our implementation has to transform API calls into ASAP calls and vica versa. Our implementation must transform application data structures into ASAP structures and vice versa.

SharkMessage de-/serialization

We defined our message datstructure with an interface: SharkMessage.

ASAP allows sending messages: sendASAPMessage. This methode requires three parameter: An application format an optional URI and a byte array that contains the actual content.

InMemoSharkMessage is the class that bridges that gap.

serialization

There is a static methode serializeMessage which produces a byte array which becomes the content of an ASAP message.

public static byte[] serializeMessage(byte[] content, CharSequence sender, Set<CharSequence> receiver,
        boolean sign, boolean encrypt, ASAPKeyStore asapKeyStore)
            throws IOException, ASAPException {
...
}

It takes all parameter which can be set with our API. It also provides a reference to an ASAPKeyStorage. This interface is provided by our SharkPKI component. Next section describes how we set up our and related components and from what source we have got that object reference.

The following code serializes all meta data of our Shark message.

    // create a ByteArrayOutputStream 
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    // serialize shark message content
    ASAPSerialization.writeByteArray(content, baos);
    // serialize shark message sender
    ASAPSerialization.writeCharSequenceParameter(sender, baos);
    // serialize shark message receiver
    ASAPSerialization.writeCharSequenceSetParameter(receiver, baos);
    // add a timestamp
    Timestamp creationTime = new Timestamp(System.currentTimeMillis());
    String timestampString = creationTime.toString();
    ASAPSerialization.writeCharSequenceParameter(timestampString, baos);
    // produce a byte array
    content = baos.toByteArray();

We use our own serialization class. We propose to do the same. Variable content contains the serialized Shark message. This content could be delivered as ASAP message.

Signing

User may want that message to be signed.

byte flags = 0;
if(sign) {
    byte[] signature = ASAPCryptoAlgorithms.sign(content, asapKeyStore);
    baos = new ByteArrayOutputStream();
    ASAPSerialization.writeByteArray(content, baos); // message has three parts: content, sender, receiver
    // append signature
    ASAPSerialization.writeByteArray(signature, baos);
    // attach signature to message
    content = baos.toByteArray();
    flags += SIGNED_MASK;
}

We use a byte variable to indicate if a message is e.g. signed. We add the signed flag in that case: flags += SIGNED_MASK.

We sign the content. ASAPCryptoAlgorithms is already implemented in the core ASAPJava library. It provides a [signing method](ASAPCryptoAlgorithms.sign(content, ASAPKeyStore). There is video that gives an introducation in basic encryption algorithms including message signing and verifying.

Again we use a ByteArrayOutputStream to compile several parts into a single byte array. First, we write the content that was produced a few lines earlier. Second, we add the signature. Finally, we have got a new content It now contains the serialized Shark message with its signature.

Encrypting

User may want that message to be encrypted.

if(encrypt) {
    content = ASAPCryptoAlgorithms.produceEncryptedMessagePackage( 
                  content, // complied Shark message (maybe with signature)
                  receiver.iterator().next(), // receiver
                  asapKeyStore);
    flags += ENCRYPTED_MASK;
}

ASAPCryptoAlgorithms provides a method that creates a symmetric encyption key. That key is used to encrypt the content. This key is encrypted with receivers public key, see a video for details.

We set a flag to remember that our contentvariable contains an encrypted message.

Produce ASAP content

Finally, we produce the content of our ASAP message by adding the flags to the content.

// serialize Shark message
baos = new ByteArrayOutputStream();
ASAPSerialization.writeByteParameter(flags, baos);
ASAPSerialization.writeByteArray(content, baos);
return baos.toByteArray();

We write our flags and the content into a byte array. That array is returned and will serve as content of an ASAP message. Deserialization follows the same steps in reverse order, see parseMessage.

Send messages

Sending a message is pretty simple thanks to our InMemoSharkMessage class.

The complete implementation can be found in SharkMessengerComponentImpl

public void sendSharkMessage(byte[] content, CharSequence uri,
    Set<CharSequence> selectedRecipients, boolean sign,
    boolean encrypt)
        throws SharkMessengerException, IOException {
     this.checkComponentRunning(); // throws an exception if component not yet running
     //....
     // serialize Shark message and send it as ASAP message
     this.asapPeer.sendASAPMessage(SHARK_MESSENGER_FORMAT, uri,
         InMemoSharkMessage.serializeMessage(
             content,
             this.asapPeer.getPeerID(),
             selectedRecipients,
             sign, encrypt,
             this.sharkPKIComponent));
       }
    } catch (ASAPException e) {
            throw new SharkMessengerException("...");
    }
}

This methods throws an exception if this component is not yet initialized (see checkComponentRunning()).

We call serializeMessage(). Our Shark message is optionally signed and encrypted and finally compiled into a byte array. That byte array is used as content of our ASAP message. Our application format is a constant value (SHARK_MESSENGER_FORMAT). Uri is parameter of this method.

this.asapPeer.sendASAPMessage sends delegates message sending to our ASAP peer. Message sent.

Receive messages

There is no extra code that deals with an incoming messages. That might sound strange at first but it is not that weird.

ASAP peers store received messages. ASAP peer offer access to that ASAPStorage. It gives an easy way to access sent and received messages with a given URI.

That is what we need. We did not implement our own message storage. We use peer’s storage instead, namely the ASAPChannel.

How does it work? We can find the following method in our API.

SharkMessengerChannel getChannel(CharSequence uri) throws SharkMessengerException, IOException;

It gives access to a SharkMessengerChannel which in turn provides a SharkMessageList. Finally, this interface allows accessing Shark message by calling SharkMessage getSharkMessage(int position, boolean chronologically)

Apparently, we had an Android Recycler View in mind when we defined and implemented that method. This class provides a convenient way to produce a list of – in that case – messages.

Use ASAP peer message storage

How do we use an ASAP storage for our messenger app. We start with the API method (full source code)

public SharkMessengerChannel getChannel(CharSequence uri) throws SharkMessengerException, IOException {
    try {
        ASAPStorage asapStorage =
            this.asapPeer.getASAPStorage(SharkMessengerComponent.SHARK_MESSENGER_FORMAT);
            ASAPChannel channel = asapStorage.getChannel(uri);
            return new SharkMessengerChannelImpl(this.asapPeer, this.sharkPKIComponent, channel);
        }
        catch(ASAPException asapException) {
            throw new SharkMessengerException(asapException);
        }
    }

First, we get access to peer's storage by calling getASAPStorage(). That storage object allows us to produce an channel object by calling getChannel(uri). Uri is parameter in our method. This method will be called if we are about to produce a list of messages of a given chat. A chat can be identified by a uri. That uri is known when we call this method.

Note: We can get a list of all uris within an ASAPStorage.

Now, we have got a channel - an object that provides a (sorted if we like) list of sent and received ASAP messages. Content of each ASAP message is a serialized Shark message. We know that because we produced it that way, see above. We use this knowledge in SharkMessengerChannelImpl.

public SharkMessengerChannelImpl(ASAPPeer asapPeer, SharkPKIComponent pkiComponent, ASAPChannel asapChannel) {
    this.asapPeer = asapPeer;
    this.pkiComponent = pkiComponent;
    this.asapChannel = asapChannel;
    }

Object of this class have access to our peer, our SharkPKI and to the specific ASAPChannel.

public SharkMessageList getMessages(boolean sentMessagesOnly, boolean ordered)
        throws SharkMessengerException, IOException {
    try {
        return new SharkMessageListImpl(this.pkiComponent, this.asapChannel, sentMessagesOnly, ordered);
    }
    catch(ASAPException e) {
        throw new SharkMessengerException(e.getLocalizedMessage(), e);
    }
}

This method produces a SharkMessageList that wraps our ASAPChannel.

...
// a (sorted) list of sent (and received) asap message
private final ASAPMessages asapMessages;
...

public SharkMessageListImpl(SharkPKIComponent pkiComponent, ASAPChannel asapChannel,
        boolean sentMessagesOnly, boolean ordered) throws IOException, ASAPException {

    //... 
    this.asapMessages = asapChannel.getMessages(false);
}

That constructor has a few more lines. Please have a look in the sources.

We just want to point out that single line of code. We produce a list of asap messages. This list contains sent and received messages. Now, we need only a single step to finally get access to our Shark messages. Our SharkMessageListImpl provides that method:

public SharkMessage getSharkMessage(int position, boolean chronologically) throws SharkMessengerException {
    try {
        // get hop list - if your are interested in it
        List<ASAPHop> hopsList = this.asapMessages.getChunk(position, chronologically).getASAPHopList();
        // most important - get access to serialized Shark messenger message
        byte[] content = this.asapMessages.getMessage(position, chronologically);
        // produce a Shark Message object from serialized data.
        return InMemoSharkMessage.parseMessage(content, hopsList, this.pkiComponent);
    }
    catch(ASAPException | IOException asapException) {
        throw new SharkMessengerException(asapException);
    }
}

The circle has closed again. We take the content of an ASAP message. That is our serialized Shark messenger message. Our InMemoSharkMessage provides a method to create a SharkMessage from a that byte array. The PKI is required to encrypt the message and verify a signature of necessary and provided.