I've decided last week to go on after all with the GoClear extension even if there were many aspects to be considered about it (like I wrote in the last blog entry). Yesterday, I've finished a basic version of the ZRTP GoClear enhancement. This is a report about the changes done to ZRTP4J and also to the SC sources along with the description of how the GoClear - GoSecure feature is implemented:
Packets added: ZrtpPacketGoClear and ZrtpPacketClearAck simple classes for the GoClear and the ClearAck messages. The first one provides the getter and setter for the clear_hmac field of the packet.
Events added to ZrtpStateClass.EventDataType: ZrtpGoClear to signal GoClear send request to the state engine and ZrtpGoSecure for signaling switching back to secure mode after GoClear.
States added (or with modified handling) to ZrtpStateClass.ZrtpStates: WaitClearAck (was already added to the enumeration but with empty events handling), UnsecuredState (the state in which the engine gets after a successful GoClear transaction), SecureState (added the GoClear related events handling to it); more information about these inside the implementation description which follows
Let's take a secured call between peer A and peer B as an example. In order to switch to unsecure mode user A clicks the padlock button (displayed as active). This triggers the send of a SecureEvent handled in CallSessionImpl, which finally gets to the ZRTPChangeStatus function's else branch which calls requestGoClear from the ZrtpTransformEngine. The request is forwarded to the ZRTP4J Zrtp class function with the same name. Here a new ZrtpGoClear event is created and sent for processing to the ZrtpStateClass state engine.
The only switch branch for processing the ZrtpGoClear event is the one from the SecureState. In case that the engine finds itself in other state when a ZrtpGoClear event is received this will be handled on the default branch of the state's function switch, meaning it will be treated in a similar way with ZrtpClose. I'm not entirely sure about this part, but it seemed to be somehow logical from the point of view of a user who cancels the call securing by clicking the paddlock button immediately after this is started - the case when the ZrtpGoClear request will reach another state than SecureState. Please note, that the ZrtpGoClear event is, as mentioned, only a request from the user to the state engine and not the event generated by the actual sending of a GoClear packet. However, if the current handling proves not to be appropriate, separate branches can be added for the ZrtpGoClear processing, to all states event handling methods.
GoClear message sending:
This part is contained in the ZrtpGoClear event processing from the SecureState. First, after receiving the ZrtpGoClear request, it is checked if GoClear-GoSecure transition is accepted for the current call. This is based, according to the ZRTP draft section 5.7.2. on the Allow Clear flags sent in the Confirm1 and Confirm2 messages during the first call securing. For this, I've added in the ZrtpPacketConfirm a setter and a getter for the Allow Clear flag. The flag received, in the peer Confirm is obtained in the prepareConfirm2, respectively in the prepareConf2Ack methods (depending on the role). For now the flag sent I've set to be true (in prepareConfirm1 and prepareConfirm2) but this might be probably enhanced with a custom GUI setting option from the user to allow or not allow secure call clearance. Both, the sent and received flag need to be true in order to permit GoClear - this is checked before going forward inside the ZrtpGoClear request processing branch, with a simple AND between the flags, performed by the Zrtp class goClearAccepted new added method. If the check returns true prepareGoClear is called from the Zrtp class. The prepareGoClear method, like the other prepare methods, has the final role to return the GoClear packet, to be sent from the state engine, after computing the clear_hmac. At this point, there might be a possible optimization of preparing the GoClear packet in advance, immediately after having the HMAC keys, or after the call is secured.
After the packet is sent, according to the ZRTP draft, the SRTP stream sending should be stopped. For this part, the afterFirstGoClearSent function from Zrtp class is called from the state engine, which forward calls stopStreaming(true) from ZrtpCallback. I've added this function to the callback interface because there is needed a method at various points in the GoClear-GoSecure interface in order to start and stop the media stream. In case of the ZrtpTransformEngine the implementation of this method sets a holdFlag. This flag is checked in the transform method, and if found to be true null is returned instead of a transformed packet. In this case, the connector's TransformOutputStream class which calls transform, returns the unmodified packet's length (as before) without sending it anymore (practically the packets are dropped). I don't know if this is the best solution for temporarily stopping the SRTP/RTP stream, but this permits to continue sending ZRTP packets "inside" the same stream (which is needed) - the holdFlag check to generate the packet dropping in the transform method from the ZrtpTransformEngine, is done only in case the packet is not a ZRTP packet. Anyway, if someone finds other solutions, there might be a good idea to compare them. After stopping the media stream, the T2 timer is started for GoClear resend as the draft says, and the current engine state is switched to WaitClearAck.
GoClear message receiving:
This is signaled to the state engine as a normal ZrtpPacket event which is processed only in the SecureState, based on the first/last char message comparison, as in the case of other packets. So, after peer B receives the packet from peer A, it generates the response packet ClearAck by calling prepareClearAck from the Zrtp class. This method performs several tasks. It checks the clear_hmac, stops the audio streaming as the other peer did and as the ZRTP draft specifies, clears the SRTP secrets by calling clearSecrets(), deletes the srtpTransformers from the ZrtpTransformEngine by calling srtpSecretsOff in order to switch to normal RTP traffic and returns a pre-created ClearAck packet to the state engine. The clearSecrets function removes the SRTP secrets and related ones as the 126.96.36.199 ZRTP draft chapter states. For now the ones included there are: srtpKeyI, srtpSaltI, srtpKeyR, srtpSaltR, pubKeyBytes, rs1IDr, rs2IDr, auxSecretIDr, rs1IDi, rs2IDi, auxSecretIDi, rs1Valid, rs2Valid, hvi, peerHvi, newRs1 and zrtpSession which is recomputed as a hash of the previous one; I'm not 100% sure if I didn't miss something or if I've added something I shouldn't had - I'll have to analyze more carefully the role of some of the enumerated ones because part of them I've placed there only based on the criteria that anyway there would be recomputed at a GoSecure toggle.
The GoClear message receiving is continued by the state engine with sending the returned ClearAck message. After this, the handleGoClear method is called from Zrtp, which forwards the call to ZrtpCallback (ZrtpTransformEngine) which again forwards it to the user callback which displays an announcement that the peer turned off the secure channel. The ZRTP draft says, I quote: "The endpoint then renders to the user an indication that the media session has switched to clear mode, and waits for confirmation from the user." I implemented this by a displaying a popup only with an OK button. However, I'm not entirely sure that "confirmation" above refers to such a simple announcement like this, or also needs providing the denial option to the user, but the draft doesn't specify exactly what should happen if the user says "no" so I've went on for the first version. After the user confirms by closing the message box and the called functions start returning, the audio stream is resumed as normal RTP from the handleGoClear in the Zrtp class. Finally, switching to UnsecuredState in the state engine concludes this part.
ClearAck message receiving:
While the above actions take place at peer B, peer A was left in WaitClearAck state after sending GoClear. This state can receive two events (besides the default Close/Error branch). First of them is the response - ClearAck - sent by peer B. This is processed as a ZrtpPacket event by the state engine and goes on like this: the GoClear timer is cancelled and after this the goClearOk function is called from the parent Zrtp class. This method calls the next functions: srtpSecretsOff which deletes the srtpTransformers, clearSecrets described above which deletes the SRTP related secrets and stopStreaming(false) which resumes the media stream as RTP from being stopped when sending GoClear. Here also should be done some GUI update (related to SAS display) which I still need to do. Finally the state engine switches to UnsecuredState.
GoClear Timer received:
The other possible event in WaitClearAck state is the Timer event triggered by a delay in receiving ClearAck after sending GoClear. In this case the GoClear is resent. If the timer expires, the clearSecrets function is called to delete the SRTP secrets as specified by the ZRTP draft. I'm not very sure in which state I should leave the peer in case this happens or if the call must be ended. At the first sight it would be logic if not ending the call to leave it in UnsecuredState, but not receiving ClearAck means probably that the other peer didn't get our GoClear so it would continue to try decoding our packets and sending coded stream. Regarding this, the state should probably remain SecureState and the srtpTransformers kept (even if the secrets are deleted, ...which doesn't make pretty much sense anyway but I've left it this way for now - at least you should be able to hear the received stream correctly...). Above all, there is also the fact that at this point the sending stream is stopped, and the draft doesn't say if it should be resumed or not. In conclusion, this timer expiration is a point which probably I must partially reconsider regarding the current implementation.
This is pretty much about the GoClear, but I'm not over yet :) . There is also the GoSecure part which I've tried to implement. (The draft states secure off - on switches should be possible alternatively during a call in case this supports GoClear)
Both A and B peers are in UnsecuredState after a successful GoClear exchange. Let's say peer A wants to switch back to secure state after a while. In order to do this the user presses the padlock button again (this should be showing secure off at this moment - padlock in gray - but this GUI part still needs a bit more coding). The way from GUI to the state engine is pretty much the same as in the GoClear case, starting with a SecureEvent handled in the CallSessionImpl and going forward as described in that case until passing a ZrtpGoSecure event finally, to be processed in the event handling function of the UnsecuredState. Here a Commit packet is prepared and sent pretty much the same as in case of sending the Commit packet when a HelloAck is received in the AckSent state. The Commit packet is generated based on the peer's Hello which was saved when the call was first secured as peerHello (in Detect and AckDetected states). The state engine goes into CommitSent state after sending the packet and starting the timer.
Commit receiving in UnsecuredState:
The Commit sent above by peer A, is received by peer B which after the GoClear should also be in UnsecuredState. For this reason the event handling function for this state has also a ZrtpPacket event branch for Commit. An own Commit packet is generated here as in the AckDetected state. I'm not 100% sure this is really necessary but the SRTP secrets related info are deleted at this moment and I left it according to Werner's comment in the AckDetected state: "Parse Hello packet and build an own Commit packet even if the Commit is not send to the peer. We need to do this to check the Hello packet and prepare the shared secret stuff. " After this part things go pretty much like for the Commit received in the WaitCommit state preparing a DHPart1 message, sending it, and switching to WaitDHPart2 state.
From this moment the state engine enters the normal transitions as when securing the call for the first time. The functionality described should cover the most of the GoClear-GoSecure additions. There are also other minor aspects not mentioned but I think this is already enough for a blog entry :). There are still some parts of which I'm not entirely sure and also some which probably can be optimized, as it can be seen during the report. Also, there are still some parts which need to be solved related to the GUI state update. Until now the padlock button was used only to send commands towards the CallSessionImpl - in this case, it must also be set according to the state of the call. There are to ways to try controlling it: one is accessing the UIService from the MediaService and the second is moving it in the plugin I've created for the label - probably using a JPanel to hold them both. I'll try the plugin option because it is the one prefferred after all and if I don't manage to get it working now I'll go for the first one for temporarily usage.
I didn't commit yet the source modifications. I'll probably do it tomorrow after I'll add also some more comments to them (and also fix the GUI part if I manage to do it until then).