aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRose Hogenson <rosehogenson@posteo.net>2026-01-25 06:36:52 -0800
committerRose Hogenson <rosehogenson@posteo.net>2026-01-25 06:36:52 -0800
commit1743be7b30655fe13ae0e60e3480f25d31eaea8f (patch)
treeda3067522c846568e8d1dcd072b6c7e31f9dca5a
parent207148233be7f30161dbbbd7d6d5a5966d1fbc36 (diff)
downloadoae2-1743be7b30655fe13ae0e60e3480f25d31eaea8f.tar.zst
Add some more comments and documentation
-rw-r--r--oae2.go35
1 files changed, 32 insertions, 3 deletions
diff --git a/oae2.go b/oae2.go
index c761d9c..adef4f7 100644
--- a/oae2.go
+++ b/oae2.go
@@ -1,11 +1,22 @@
-// Pacakage oae2 implements “Online Authenticated-Encryption” based on the
-// influential paper
+// Pacakage oae2 implements “Online Authenticated-Encryption” (also known as
+// streaming AEAD) based on the influential paper
// “[Online Authenticated-Encryption and its Nonce-Reuse Misuse-Resistance]” by
// Hoang et al.
//
// Please do not use this package; it likely has critical
// security vulnerabilities.
//
+// # Encrypted stream format
+//
+// The encrypted streams written by this package start with a 32 byte random
+// salt, followed by a number of encrypted segments each of length
+// segmentSize + 16. That is, each encrypted segment has an overhead of 16
+// bytes for the authentication tag. Each segment is encrypted using
+// AES-256-GCM, using a key derived from the given key and the random salt
+// using HKDF-HMAC-SHA256.
+//
+// The encrypted stream format may change in the future.
+//
// [Online Authenticated-Encryption and its Nonce-Reuse Misuse-Resistance]: https://eprint.iacr.org/2015/189.pdf
package oae2
@@ -202,6 +213,7 @@ func (w *Writer) Close() error {
return nil
}
+// We avoid bufio.Reader to minimize extra allocations.
type bufReader struct {
r io.Reader
nextByte byte
@@ -247,7 +259,8 @@ type Reader struct {
}
// NewReader returns a Reader that wraps r and decrypts the data using key in
-// chunks of size segmentSize. NewReader panics if segmentSize <= 0.
+// chunks of size segmentSize. segmentSize must match the segment size that was
+// used to write the encrypted stream. NewReader panics if segmentSize <= 0.
func NewReader(r io.Reader, key []byte, segmentSize int) *Reader {
if segmentSize <= 0 {
panic("oae2.NewReader: segmentSize must be strictly greater than 0")
@@ -262,6 +275,16 @@ func NewReader(r io.Reader, key []byte, segmentSize int) *Reader {
func (r *Reader) initialize() error {
r.initialized = true
+ // N.B. there's a subtle bug lurking here: for an input stream of
+ // exactly 32 bytes, it's important that we reject this stream since it
+ // doesn't have an authentication tag. So if we naively read exactly 32
+ // bytes inside initialize, then Read will see that the underlying
+ // reader is at EOF, and won't be able to distinguish this from a valid
+ // EOF case. Fortunately we can easily work around this by reading one
+ // extra byte here: since the shortest possible encrypted stream is
+ // 32 + 16 bytes, every valid stream will have an extra byte for us
+ // here, and other than on the first segment, Read always knows
+ // precisely whether the stream is at EOF.
buf := make([]byte, saltSize+1)
if _, r.err = io.ReadFull(&r.r, buf); r.err != nil {
if r.err == io.EOF {
@@ -282,8 +305,14 @@ func (r *Reader) init() error {
}
func (r *Reader) fillBuf() error {
+ // It's important that we know whether this is the last segment, to set
+ // the appropriate byte in the IV. Some other implementations just try
+ // both options, but I think we can be a little bit more precise and
+ // read an extra byte here to be sure if we're at the end.
n, err := io.ReadFull(&r.r, r.buf[:r.segmentSize+aeadOverhead+1])
if err != nil && err != io.ErrUnexpectedEOF {
+ // Don't set r.err to EOF here, since we might Seek and reset
+ // the stream, but r.err is unrecoverable.
if err != io.EOF {
r.err = err
}