This spec describes the operation of the Strobe framework. It only covers the symmetric portion. For applications including elliptic curve crypto, see the examples page.
AD
: Provide associated dataKEY
: Provide cipher keyCLR
: Send or receive cleartext dataENC
: Send or receive encrypted dataMAC
: Send or receive message authentication codePRF
: Extract hash / pseudorandom dataRATCHET
: Prevent rollbackThis specification describes Strobe version 1.0.2.
It has been revised for clarity since that version was released,
and to add a usage hint for meta_RATCHET
. Special thanks
to David Wong for his feedback on the specification.
Strobe is useful for building both symmetric cryptosystems and protocols. For simplicity, these are both referred to as "protocols". For example, in an encryption protocol, Alice sends an encrypted and authenticated message to Bob, but Bob does not send any messages back.
Strobe protocols exchange data over some sort of transport. For most protocols
To disambiguate the two parties of a network protocol, Strobe assigns them each a role as an initiator or responder.
Parties start out as undecided, and become an initiator or responder if and when they send or receive a message via the transport.
Strobe operates exclusively on bytes, which are elements of the set [0,...,255]. However, sometimes a length field is required which is larger than one byte. Furthermore, in extensions of Strobe, such as the key tree, we will need elements smaller than one byte. In either case, Strobe follows a little-endian convention.
Variables are formatted like this.
Inline code is formatted like this.
Blocks of code are formatted like this.
Non-normative comments are formatted like this.
Non-normative warnings are formatted like this.
There are several paremeters in the Strobe framework. It can use many different rates, permutations and padding modes. These are detailed in the paper. This specification only describes the following recommended instances:
Strobe-128/1600 and Strobe-256/1600 are based on the cSHAKE specification from NIST.
Let b be either 400, 800 or 1600. Let F be the function Keccak-f[b]. Let N = b/8. Strobe treats F as a function which takes as input an array of N bytes and returns another array of N bytes.
Let sec be a target security level, either 128 or 256 bits. Strobe has 2*sec bits of secret state. As a result, it is somewhat stronger than a block cipher with sec-bit key. Depending on the exact protocol, it may achieve security comparable to a 2*sec-bit block cipher (as in this paper). In other scenarios — particularly when used as an unkeyed hash — it only has sec bits of security. A future version might add a way to change the rate after a key has been entered.
Let R = N - (2*sec)/8 - 2. This will be the number of bytes in a Strobe block. It is required that 1 ≤ R < 254. This means that b = 400 is incompatible with sec = 256. Obviously R ≥ 1 is required so that Strobe can get any work done at all. The requirement that R ≤ 254 is so that offsets can be represented in one byte. In the existing protocol, this can result in offsets up to R, which would imply that R ≤ 255. The requirement R ≤ 254 is a hedge to allow future modifications. In any case, it is automatically true for any Keccak-based protocol, because R < 1600/8 = 200.
Let X, Y and Z be single digits, representing a major, minor and patch version of Strobe.
The parameters below define the protocol framework called "Strobe-Keccak-sec/b-v1.0.2", which is abbreviated to "Strobe-sec/b". In general, the name would be "Strobe-F-sec/b-vX.Y.Z".
A Strobe object has the following state variables:
None
.
This variable describes the role of this party in the protocol. The role
begins as undecided (I0 = None
),
and stays that way until the party either sends or receives a message on
the transport. At that point the party's role becomes initiator
(with I0 = 0) if it sent the message, or responder
(I0 = 1) if it received the message.
The purpose of I0 is to keep protocol transcripts
consistent. Strobe hashes not only the messages
that are sent, but also metadata about who sent them. It would be no good if
Alice hashed "I sent a message" and Bob hashed "I received a message", because
their hashes would be different. Instead, they hash metadata amounting to
"The initiator sent this message" or "the responder sent this message.
The initial state of the object is as follows:
st = F( [0x01, R+2, 0x01, 0x00, 0x01, 0x60] +
ascii("STROBEvX.Y.Z"))
Here +
denotes concatenation, as in Python.
The X,
Y, and Z are the major, minor and patch
version of the Strobe protocol.
The trailing ...
is means enough zeros to fill out
the domain of the function F
This format is chosen for compatibility with
cSHAKE.
The description of the initial state involves multiple calls to F
if R is less than 15 bytes,
but this isn't the case for any of the recommended instances. The
Python code below covers the general case.
pos = posbegin = 0
I0 = None
Following this initialization, the first operation must be a meta-AD
operation whose data is a protocol-specific string. This separates
Strobe instances from each other. This operation is also
known as customization, personalization, domain separation or diversification.
A Strobe protocol execution is composed of a sequence of operations. There may be any number of operations in any order. Which operations are performed may be determined at runtime. Each operation can process an arbitrary amount of data, but always in multiples of one byte. The data is processed in a streaming fashion, one byte at a time. The protocol can determine the amount of data to process on the fly, even after an operation has begun. In python notation, the sequence
ctx.send_ENC("A long"); ctx.send_ENC(" message",more=True);
will do exactly the same thing as
ctx.send_ENC("A long message");
but not the same thing as
ctx.send_ENC("A long"); ctx.send_ENC(" message");
This last piece of code performs two separate ENC
operations, whereas
the others each only do one operation.
Strobe's operations are low-level, and a protocol
will be a combination of many of them.
For example, an AEAD system might be comprised of a key (KEY
),
an associated datum (AD
), an encrypted message
(ENC
) and a
message authentication code (MAC
). It might use further operations
for framing, context and metadata. This specification first describes the operations, and then
recommends how to combine them.
Strobe is based on Keccak-f.
Keccak-f is somewhat slow in software, and it can have
a rather wide block size — up to 168 bytes of rate. Strobe
operations may be rather small. In particular, they are used for framing data,
which is generally only a few bytes. To reduce overhead, Strobe
packs multiple operations into one block when possible. It is not always possible,
though. In particular, operations such as ENC
and MAC
must produce output that depend on all previous inputs. For such operations,
Strobe runs F and begins a new block.
The paper specifies a variant, called "Strobe lite", which always begins a new block for every new operation. This is designed for lightweight hardware, where F is probably much smaller and there is little gain from packing multiple operations into one block.
Strobe supports 7 low-level operations. Of these,
the 3 which use the transport exist in send
and recv
directions.
If Alice sends a message to Bob, Alice will use a send
operation
(such as send_ENC
) and Bob will use the corresponding recv
operation (recv_ENC
). Strobe takes this into
account when hashing the operations, so that Alice's duplex state will match Bob's
when they execute corresponding operations.
For any operation, there is a corresponding "meta
" variant. The
meta
operation works exactly the same way as the ordinary operation.
The two are distinguished only in an "M
" bit that is hashed into
the protocol transcript for the meta
operations. This is used to
prevent ambiguity in protocol transcripts. This specification describes uses
for certain meta
operations. The others are still legal,
but their use is not recommended.
The usage of the meta
flag is described below in
Section 6.3.
The operations are:
AD
: Provide associated dataKEY
: Provide cipher keyCLR
: Send or receive cleartext dataENC
: Send or receive encrypted dataMAC
: Send or receive message authentication codePRF
: Extract hash / pseudorandom dataRATCHET
: Prevent rollbackThis section describes the rough behavior of these operations, not how they are implemented. The exact behavior and implementation are described in Section 7. Each operation also lists the flags which describe it. Their meaning is described in Section 6.2.
AD
: Provide associated dataA
.
The AD
operation adds associated data to the state.
This data must be known to both parties, and will not be transmitted.
Future outputs from the Strobe object will depend
on the supplied data.
meta_AD
operation describes the protocol's interpretation
of the following operation.KEY
: Provide cipher keyAC
.
The KEY
operation sets a symmetric key.
If there is already a key, the new key will be cryptographically combined with it.
This key will be used to produce all future cryptographic outputs from
the Strobe object.
CLR
: Send or receive cleartext dataAT
or IAT
.
send_CLR
sends a message in clear text.
recv_CLR
receives a message in clear text.
send_meta_CLR
and recv_meta_CLR
are used to send and receive framing data.
The recv_CLR
and recv_meta_CLR
operations don't verify
the integrity of the
incoming message. For this, follow send_CLR
with send_MAC
on the sending side,
and follow recv_CLR
with recv_MAC
on the receiving side.
In some scenarios, one cannot check a MAC for protocol or performance
reasons. For example, until the two parties share a secret key, a MAC defends only
against accidental corruption and not against malicious modification.
Strobe explicitly supports MACs on framing data. However,
to save bandwidth, most protocols do not MAC their framing data until they have
sent/received the framed data as well. This means that extra care must be used
with framing data, especially lengths.
ENC
: Send or receive encrypted dataACT
or IACT
.
send-ENC
: Run F to begin a new block.
Xor the plaintext with the state, which produces
both a new state and a ciphertext. Send the ciphertext to the other party.recv-ENC
: Run F to begin a new block.
Receive a ciphertext, and xor it with the state
to obtain a plaintext. Bytes of the state are overwritten by bytes of the ciphertext.send_ENC
encrypts a message and sends it to the transport.
recv_ENC
receives a message from the transport and decrypts it.
The encryption does not include authentication. The length of the encrypted
message is the same as the length of the plaintext message.
send_meta_ENC
and recv_meta_ENC
are used for encrypted framing data.
Strobe's encryption mode keeps the message confidential (except for its length) so long as the session transcript up to this point is secret and, for the sender, unique.
send_ENC
operation. This requires nonces, unless the
protocol has already exchanged one-time-use ephemeral keys.
recv_ENC
doesn't require uniqueness for security, so long as a
recv_MAC
operation is run before the received data is used.
The recv_ENC
and recv_meta_ENC
operations don't verify
the integrity of the
incoming message. For this, use send_MAC
after send_ENC
on the sending side,
and recv_MAC
after recv_ENC
on the receiving side.
The receiving side must run
recv_MAC
before using the decrypted message.
Newcomers to cryptography often assume that an attacker can't modify encrypted
messages without turning them into gibberish. This isn't true for most cipher
modes, and it isn't true for Strobe. The
MAC
operation really is necessary.
Strobe's encryption mode requires unique nonces for
security.
In other words, the operations before the ENC
operation
must be unique. In a two-party protocol, make sure that a party has
contributed a unique value (such as a random nonce or a Diffie-Hellman ephemeral)
to the protocol before using send_ENC
. It is not necessary to
contribute a unique value before using recv_ENC
, except to
defend against physical side channels such as DPA.
Protocols are allowed to use ENC
before any KEY
has
been entered. This usually isn't secure as an encryption mode. For technical
reasons, the recommended Schnorr signature mode
does this whether or not a key has been entered.
MAC
: Send or receive message authentication codeCT
or ICT
.
send-MAC
: Run F to begin a new block.
Send bytes of the state to the other party.recv-ENC
: Run F to begin a new block.
Receive bytes and check that they are the same as the bytes of your state.
If not, then the session has been corrupted.send_MAC
computes and sends a message authentication code (MAC).
recv_MAC
receives and checks a MAC. If the MAC doesn't match,
the receiving party aborts the protocol.
send_meta_MAC
and recv_meta_MAC
do the same thing
as send_MAC
and recv_MAC
. They are appropriate
for checking the integrity of framing data.
A MAC
operation is ineffective if it is too short. That is,
an adversary can just guess a B-byte MAC value with probability
2-8B. A 16-byte or longer MACs is suitable for
protocols on powerful devices, and 8-byte or longer MAC is suitable for
constrained devices. Very constrained environments might use even shorter
MACs, but be aware that a short MAC may be practical to guess.
When retrofitting security on top of legacy embedded protocols, designers
might not have enough bits to send a secure MAC
. It is possible
to send a shorter MAC
in each packet, with the hope that an
attack will be detected within a few packets. The Strobe
framework allows this, but of course it is very dangerous.
Don't try it without an expert analysis.
Checking a MAC must be done in constant time to prevent side-channel attacks. This is particularly salient when checking a MAC that is streaming in one byte at a time. It is very important not to indicate an error until the MAC operation is actually complete. Otherwise an attacker will learn information about the correct MAC. If the attacker can make several attempts, this side channel will lead to an easy compromise.
Data isn't automatically trustworthy just because it has been MAC'd or signed. At most, the MAC will indicate who sent the data. But that other party might still be evil, or might have been compromised.
Strobe's operations all support byte-by-bytes streaming,
and do not
require that the length be known in advance. The MAC operation is no exception.
That means in particular that truncating a MAC produces a valid, shorter MAC.
This problem can and should be avoided by using a composite operation which
includes the MAC's length in the metadata, so that the MAC will depend on its
length.
However, this note doesn't rise to the level of a warning. Most protocols will
use fixed-length MACs, but some will not. Suppose the two parties somehow
disagree on how long their MACs are. Suppose that Bob thinks the MACs are 8
bytes, but Alice thinks they're 10 bytes. Then the first message that Alice sends to
Bob will verify with a truncated MAC, but any message that either sends later
in the protocol will fail, because of the different transcript before that point.
This is unlikely to cause a huge security problem.
The reverse direction is also problematic: if Alice thinks the MACs are 8 bytes,
but Bob thinks they're 10 bytes, then an attacker can extend Alice's MAC to
one that Bob will accept with probability 2^-16. Again, any future MAC will
fail with high probability.
Strobe's MAC
operations
don't require session uniqueness.
Repeating a nonce will compromise send_ENC
, but (unlike AES-GCM
and Poly1305) it will not compromise send_MAC
or recv_MAC
.
This only applies to attackers trying to find MAC
s on
new data. If an attacker replays the same MAC
after the same
data in an identical session, then of course the MAC
will verify.
PRF
: Extract hash / pseudorandom dataIAC
.
PRF
extracts pseudorandom data which is a deterministic function of
the state. This data can be treated as a hash of all preceeding operations,
messages and keys. For example, the following code hashes a single block of data:
ctx = new strobe(proto="example hash") ctx.AD("message to be hashed") the_hash = ctx.PRF(output_length)
One can also hash multiple blocks of data, for example:
ctx = new strobe(proto="example hash") ctx.AD("message to") ctx.AD(" be") ctx.AD(" hashed") the_hash = ctx.PRF(output_length)
which produces a different result from the previous example, because Strobe hashes where the operations begin and end in addition to their data.
Just as with a MAC
, the PRF
operation supports streaming,
and a shorter PRF
call will return a prefix of a longer one. Use
a composite operation that inputs the length of the
PRF
first.
RATCHET
: Prevent rollbackC
.
RATCHET
has no input other than a length L, and no output.
Instead, it modifies the state in an irreversible way.
Strobe's F is Keccak-f, which is a permutation. If an attacker compromises the state at some point in the protocol (e.g. with a side channel attack or an application exploit), then the attacker can use F-1 to solve for states at earlier points in the protocol.
The RATCHET
operation prevents this by zeroizing L bytes of the state.
Zeroizing the whole state would destroy the key, so RATCHET
only
zeroizes up to R bytes at a time, calling F in between.
In order to reverse the
RATCHET
operation, the attacker
would have to guess what the bytes were before zeroizing. Setting
L = sec/8 bytes is sufficient when R ≥
sec/8. That is, set L to 16 bytes or 32 bytes for
Strobe-128/b and
Strobe-256/b, respectively.
If L is set to a very large number, then this operation wastes time
by calling F many times in a row. This makes it suitable for a
password-based key derivation function (PBKDF), with security comparable
to PBKDF2.
Modern PBKDFs such as scrypt and Argon2 are designed to be memory-hard, meaning
that they require a large memory with high bandwidth to perform efficiently.
Memory hardness makes it harder to guess passwords by brute force using GPUs or ASICs.
Using RATCHET
as a PBKDF isn't memory-hard. But of course, a tiny
device with constrained memory can't efficiently compute a memory-hard function,
so this may be the best you can do.
For all the recommended Strobe variants, R ≥
sec/8. In non-recommended variants with R < sec/8, this
operation is ineffective at preventing rollback
because it only erases R bytes at a time. Instead,
extract L bytes using PRF
, and then stir them back in
with KEY
. In this scenario, RATCHET
is still suitable
to waste time, but the PRF/KEY
ratcheting operation must be used
afterward to prevent rollback.
meta_RATCHET
with length either 0 or R
is suggested as a way to forcibly align to the beginning of a block,
which has a few niche use cases.
Note that length=0
will not erase any bytes
and will not prevent rollback: it only aligns to the beginning of
the block. On the other hand, this usage is faster and doesn't
require knowledge of R, which is otherwise abstracted
from the designer.
Setting length=R
prevents rollback, and
allows memory-constrained implementations to cache the state before
the call to F, which takes more time but less memory.
The behavior of each of Strobe's operations is defined completely by 6 features, called flags. The operation is encoded as one byte, where the least significant 6 bits are its flags. The flags and their encodings are as follows:
I = 1<<0
, "inbound". If set, this flag means that the
operation moves data from the transport, to the cipher, to the application.
An operation without the I
flag set is said to be "outbound".
The I
flag is clear on all send
operations,
and set on all recv
operations.
If Alice sends a message to Bob, then it is outbound for Alice and inbound
for Bob.
A = 1<<1
, "application". If set, this flag means that
the operation has data coming to or from the application side.
I
and A
both set outputs
bytes to the application.A
set but I
clear takes input from the application.C = 1<<2
, "cipher". If set, this flag means that the
operation's output depends cryptographically on the Strobe
cipher state. For operations which don't have I
or T
flags
set, neither party produces output with this operation. In that case, the
C
flag instead means that the operation acts as a rekey or ratchet.
T = 1<<3
, "transport". If set, this flag means that the
operation sends or receives data using the transport. An operation has
T
set if and only if it has send
or recv
in its name.
I
and T
both set
receives data from the transport.T
set but I
clear sends data to the transport.M = 1<<4
, "meta". If set, this flag means that the
operation is handling framing, transcript comments or some other sort
of protocol metadata. It doesn't affect how the operation is performed.
This is intended to be used as described below
in Section 6.3.
K = 1<<5
, "keytree". This flag is reserved for a certain
protocol-level countermeasure against side-channel analysis. It does affect
how an operation is performed. This specification does not describe its use.
For all operations in this specification, the K
flag must be clear.
1<<6
and 1<<7
are reserved for
future versions.Each operation type is uniquely described by a combination of the above flags:
Operation | Flags | |||
---|---|---|---|---|
AD |
A | |||
KEY |
A | C | ||
PRF |
I | A | C | |
send_CLR |
A | T |
||
recv_CLR |
I | A | T |
|
send_ENC |
A | C | T |
|
recv_ENC |
I | A | C | T |
send_MAC |
C | T |
||
recv_MAC |
I | C | T |
|
RATCHET |
C |
The meta
variants of the operations are the same as the
non-meta
variants, but they additionally have the M
flag set.
The other 6 combinations of the IACT
flags are legal and their behavior
is well-defined, but they aren't as useful.
Composite operations enable protocols to comply with the
Horton principle:
"authenticate what is being meant, not what is being said". They do this by
adding metadata to the operation which describes its meaning. How
to encode that meaning is up to the protocol, so the metadata may be in any
number of meta
operations of any lengths. The most straightforward
encoding would be a single-byte tag describing the meaning of the
data, and a fixed number of bytes describing the length of the data in some
particular endianness.
The metadata is processed before the data. This means that PRF
outputs,
encrypted data and MAC
s will depend on the metadata as
well as on previous operations.
In a complex protocol, it might be possible to send more than
one kind of message at a given point. It is safe for the recipient to read the
framing data — either all at once or byte-by-byte — to determine
which kind of message to receive. However, the recipient must only accept a
message if it is appropriate at the current stage of the protocol.
A composite operation is a sequence of two or more operations, exactly one of which is designated as the payload. The sequence consists of:
meta_AD
, meta_CLR
or meta_ENC
operations containing information about what the payload operation means to
the protocol. For protocols which use framing data on the transport, this
is generally a single meta_CLR
containing that framing data.
If the payload operation uses the cipher's output, then these meta
operation(s) should uniquely encode the length of the payload as well as its
meaning. This prevents mistakes that could lead to truncation attacks.
meta_MAC
operation to protect the framing data.
Most protocols will omit this to save bandwidth. It may be a useful operation
if the recipient of the message will have to do something expensive depending
on the metadata, such as allocating large amounts of memory. Remember that
a meta_MAC
doesn't help if the sender is malicious.
meta
operation.
MAC
operation. In some circumstances, it is
also important to provide a MAC
before the end of the flow. Consult
a cryptographer if you are unsure.
MAC
truncation or
extension attacks, it makes
sense to supply the MAC
length in a meta_AD
transaction. It should still be fixed.MAC
s below a fixed minimum
length, which should not be less than 8 bytes.
A composite operation consists of one or more meta
operations,
followed by exactly one non-meta
operation. It is recommended
not to use meta
operations except to implement these composite
operations. That way, the protocol transcript can be parsed uniquely
into a sequence of (composite and possibly non-composite) operations.
In a complex protocol, it might be possible to send more than one kind of message at a given point. It is safe for the recipient to read the framing data — either all at once or byte-by-byte — to determine which kind of message to receive. However, the recipient must only accept a message if it is appropriate at the current stage of the protocol.
This section describes formally what each operation does and how it is implemented. The implementation is described by Python code. [TODO: and math?]
Strobe is a
duplex construction,
so its core routine _duplex()
is somewhat unsurprising. This
routine takes input data and three binary arguments:
cbefore, cafter and forceF.
If cafter is set, then the data is modified by the duplex construction after absorbing it. This is used for encryption. If cbefore is set, the data is modified by the duplex construction before absorbing it. This is used for decryption. There is no case where both cbefore and cafter are set.
Certain operations need to run F afterward, in order to begin a new block.
For brevity, they do this by passing a forceF argument to _duplex()
.
def _duplex(self, data, cbefore=False, cafter=False, forceF=False): # This is an internal function. It's not part of STROBE's API. assert not (cbefore and cafter) # Copy data, and convert string or int to bytearray # This converts an integer n to an array of n zeros data = bytearray(data) for i in range(len(data)): if cbefore: data[i] ^= self.st[self.pos] self.st[self.pos] ^= data[i] if cafter: data[i] = self.st[self.pos] self.pos += 1 if self.pos == self.R: self._runF() if forceF and self.pos != 0: self._runF() return data
When the rate is exceeded, or when beginning an operation that uses the cipher state, Strobe runs the sponge function F on the state. This begins a new block. Because posbegin records the beginning of an operation within the block, Strobe absorbs it and resets it when beginning a new block:
def _runF(self):
# This is an internal function. It's not part of STROBE's API.
if self.initialized:
self.st[self.pos] ^= self.posbegin
self.st[self.pos+1] ^= 0x04
self.st[self.R+1] ^= 0x80
self.st = self.F(self.st)
self.pos = self.posbegin = 0
The 0x04
and 0x80
pads are cSHAKE's output padding mode. For simplicity,
this is applied on every block whether it has output or not.
[posbegin]
is Strobe's padding mode on top
of cSHAKE's padding.
Strobe uses cSHAKE's domain separation to ensure that it is
distinct from all other uses of Keccak. It then uses its
own internal separation — a meta_AD
operation — to
ensure that every Strobe-based protocol is distinct. This
first separation uses cSHAKE's specified padding mode instead of
Strobe's padding. This is
why _runF
checks self.initialized.
Strobe's initial state is set using cSHAKE's domain separation. The cSHAKE domain separation string is S = "STROBEv1.0.2". The NIST separation string is N = "", because Strobe wasn't designed by NIST.
def __init__(self, proto, F = KeccakF(1600), sec = 128): self.pos = self.posbegin = 0 self.I0 = None self.F = F self.R = F.nbytes - sec//4 self.cur_flags = None # Domain separation doesn't use Strobe padding self.initialized = False self.st = bytearray(F.nbytes) domain = bytearray([1,self.R,1,0,1,12*8]) \ + bytearray("STROBEv1.0.2") self._duplex(domain, forceF=True) # cSHAKE separation is done. # Turn on Strobe padding and do per-proto separation self.R -= 2 self.initialized = True self.operate(A|M, proto)
Beginning an operation absorbs the old posbegin state variable, and then sets it to where this operation began. It then absorbs the operation, adjusted so that the sender and receiver will compute the same value.
def _beginOp(self, flags): # This is an internal function. It's not part of STROBE's API. # Adjust direction information so that sender and receiver agree if flags & T: if self.I0 is None: self.I0 = flags & I flags ^= self.I0 # Update posbegin oldbegin, self.posbegin = self.posbegin, self.pos+1 self._duplex([oldbegin,flags], forceF = flags&(C|K))
Forcing F when flags & (C|K)
is nonzero ensures that
any input — including the operation's flags —
will affect the output of C
-flagged operations
like ENC
, MAC
and PRF
. It also
ensures that the rekeying operations
KEY
and RATCHET
begin
at the start of a block. This is important to prevent rollback attacks: if a
KEY
or RATCHET
were split across blocks, then
an adversary who recovered a later state could split the work of
brute-forcing the earlier state into separate attacks on each block.
To perform an operation, Strobe first runs the beginning-of-operation code. It then duplexes the data with cbefore and cafter set according to the operation's flags. Finally, it decides what to do with the output: return it, ignore it, or check it as a MAC value.
The implementation below supports a more flag for streaming purposes. Setting more processes the given data as a continuation of the previous operation, instead of as a new operation.
def operate(self, flags, data, more=False): assert not (flags & (K|1<<6|1<<7)) # Not implemented here if more: assert flags == self.cur_flags else: self._beginOp(flags) self.cur_flags = flags if (flags & (I|T) != (I|T)) and (flags & (I|A) != A): # Operation takes no input, only a length assert isinstance(data,int) # This is because in Python, bytearray(n) returns an array # of n zeros. # # In other languages, you might make a function which # takes a length and a nullable pointer, or which is # overloaded to take either an integer or a byte array. # The actual processing code is just duplex cafter = (flags & (C|I|T)) == (C|T) cbefore = (flags & C) and not cafter processed = self._duplex(data, cbefore, cafter) # Determine what to do with the output. if (flags & (I|A)) == (I|A): # Return data for the application return processed elif (flags & (I|T)) == T: # Return data for the transport. # A fancier implementation might send it directly. return processed elif (flags & (I|A|T)) == (I|T): # Check MAC: all output bytes must be 0 assert not more # See the side-channel warning failures = 0 for byte in processed: failures |= byte if failures != 0: raise AuthenticationFailed() return bytearray() else: # Operation has no output return bytearray()
Each of the defined operations from Section 6 is implemented as a call to operate
.
Note that some operations take an array of data whereas some just take a length.
That these two types are handled the same way is an idiom of Python. Other languages would take
length as an integer and data as an array of bytes.
def AD(self, data, more=False): self.operate(A, data, more) def KEY(self, data, more=False): self.operate(A|C, data, more) def send_CLR(self, data, more=False): return self.operate(A|T, data, more) def recv_CLR(self, data, more=False): return self.operate(I|A|T, data, more) def send_ENC(self, data, more=False): return self.operate(A|C|T, data, more) def recv_ENC(self, data, more=False): return self.operate(I|A|C|T, data, more) def send_MAC(self, length, more=False): return self.operate(C|T, length, more) def recv_MAC(self, data, more=False): self.operate(I|C|T, data, more) def PRF(self, length, more=False): return self.operate(I|A|C, length, more) def RATCHET(self, length, more=False): self.operate(C, length, more)
KEY
operation has very similar semantics toAD
. Both of them absorb data without transmitting it, and affect all future operations. That said,KEY
differs fromAD
in three ways:KEY
ratchets the state to preserve forward secrecy. It does this by overwriting state bytes with the new key instead of xoring. This mitigates attacks if an attacker somehow compromises the application later. Strobe doesn't do this with most other operations, such asAD
, because it would slightly reduce the entropy of the state.KEY
starts a new block internally. This is needed for ratcheting, and might also help with a future security analysis in the standard model.KEY
differently fromAD
.