@@ -8,15 +8,42 @@ package stream
88import (
99 "bytes"
1010 "crypto/cipher"
11+ "encoding/binary"
1112 "errors"
1213 "fmt"
1314 "io"
15+ "sync/atomic"
1416
1517 "golang.org/x/crypto/chacha20poly1305"
1618)
1719
1820const ChunkSize = 64 * 1024
1921
22+ func EncryptedChunkCount (encryptedSize int64 ) (int64 , error ) {
23+ chunks := (encryptedSize + encChunkSize - 1 ) / encChunkSize
24+
25+ plaintextSize := encryptedSize - chunks * chacha20poly1305 .Overhead
26+ expChunks := (plaintextSize + ChunkSize - 1 ) / ChunkSize
27+ // Empty plaintext, the only case that allows (and requires) an empty chunk.
28+ if plaintextSize == 0 {
29+ expChunks = 1
30+ }
31+ if expChunks != chunks {
32+ return 0 , fmt .Errorf ("invalid encrypted payload size: %d" , encryptedSize )
33+ }
34+
35+ return chunks , nil
36+ }
37+
38+ func PlaintextSize (encryptedSize int64 ) (int64 , error ) {
39+ chunks , err := EncryptedChunkCount (encryptedSize )
40+ if err != nil {
41+ return 0 , err
42+ }
43+ plaintextSize := encryptedSize - chunks * chacha20poly1305 .Overhead
44+ return plaintextSize , nil
45+ }
46+
2047type DecryptReader struct {
2148 a cipher.AEAD
2249 src io.Reader
@@ -135,6 +162,12 @@ func incNonce(nonce *[chacha20poly1305.NonceSize]byte) {
135162 panic ("stream: chunk counter wrapped around" )
136163}
137164
165+ func nonceForChunk (chunkIndex int64 ) * [chacha20poly1305 .NonceSize ]byte {
166+ var nonce [chacha20poly1305 .NonceSize ]byte
167+ binary .BigEndian .PutUint64 (nonce [3 :11 ], uint64 (chunkIndex ))
168+ return & nonce
169+ }
170+
138171func setLastChunkFlag (nonce * [chacha20poly1305 .NonceSize ]byte ) {
139172 nonce [len (nonce )- 1 ] = lastChunkFlag
140173}
@@ -312,3 +345,102 @@ func (r *EncryptReader) feedBuffer() error {
312345
313346 return nil
314347}
348+
349+ type DecryptReaderAt struct {
350+ a cipher.AEAD
351+ src io.ReaderAt
352+ size int64
353+ chunks int64
354+ cache atomic.Pointer [cachedChunk ]
355+ }
356+
357+ type cachedChunk struct {
358+ off int64
359+ data []byte
360+ }
361+
362+ func NewDecryptReaderAt (key []byte , src io.ReaderAt , size int64 ) (* DecryptReaderAt , error ) {
363+ aead , err := chacha20poly1305 .New (key )
364+ if err != nil {
365+ return nil , err
366+ }
367+
368+ // Check that size is valid by decrypting the final chunk.
369+ chunks , err := EncryptedChunkCount (size )
370+ if err != nil {
371+ return nil , err
372+ }
373+ finalChunkIndex := chunks - 1
374+ finalChunkOff := finalChunkIndex * encChunkSize
375+ finalChunkSize := size - finalChunkOff
376+ finalChunk := make ([]byte , finalChunkSize )
377+ if _ , err := src .ReadAt (finalChunk , finalChunkOff ); err != nil {
378+ return nil , fmt .Errorf ("failed to read final chunk: %w" , err )
379+ }
380+ nonce := nonceForChunk (finalChunkIndex )
381+ setLastChunkFlag (nonce )
382+ plaintext , err := aead .Open (finalChunk [:0 ], nonce [:], finalChunk , nil )
383+ if err != nil {
384+ return nil , fmt .Errorf ("failed to decrypt and authenticate final chunk: %w" , err )
385+ }
386+ cache := & cachedChunk {off : finalChunkOff , data : plaintext }
387+
388+ plaintextSize := size - chunks * chacha20poly1305 .Overhead
389+ r := & DecryptReaderAt {a : aead , src : src , size : plaintextSize , chunks : chunks }
390+ r .cache .Store (cache )
391+ return r , nil
392+ }
393+
394+ func (r * DecryptReaderAt ) ReadAt (p []byte , off int64 ) (n int , err error ) {
395+ if off < 0 || off > r .size {
396+ return 0 , fmt .Errorf ("offset out of range [0:%d]: %d" , r .size , off )
397+ }
398+ if len (p ) == 0 {
399+ return 0 , nil
400+ }
401+ chunk := make ([]byte , encChunkSize )
402+ for len (p ) > 0 && off < r .size {
403+ chunkIndex := off / ChunkSize
404+ chunkOff := chunkIndex * encChunkSize
405+ encSize := r .size + r .chunks * chacha20poly1305 .Overhead
406+ chunkSize := min (encSize - chunkOff , encChunkSize )
407+
408+ cached := r .cache .Load ()
409+ var plaintext []byte
410+ if cached != nil && cached .off == chunkOff {
411+ plaintext = cached .data
412+ } else {
413+ nn , err := r .src .ReadAt (chunk [:chunkSize ], chunkOff )
414+ if err == io .EOF {
415+ if int64 (nn ) != chunkSize {
416+ err = io .ErrUnexpectedEOF
417+ } else {
418+ err = nil
419+ }
420+ }
421+ if err != nil {
422+ return n , fmt .Errorf ("failed to read chunk at offset %d: %w" , chunkOff , err )
423+ }
424+ nonce := nonceForChunk (chunkIndex )
425+ if chunkIndex == r .chunks - 1 {
426+ setLastChunkFlag (nonce )
427+ }
428+ plaintext , err = r .a .Open (chunk [:0 ], nonce [:], chunk [:chunkSize ], nil )
429+ if err != nil {
430+ return n , fmt .Errorf ("failed to decrypt and authenticate chunk at offset %d: %w" , chunkOff , err )
431+ }
432+ r .cache .Store (& cachedChunk {off : chunkOff , data : plaintext })
433+ }
434+
435+ plainChunkOff := int (off - chunkIndex * ChunkSize )
436+ copySize := min (len (plaintext )- plainChunkOff , len (p ))
437+ copy (p , plaintext [plainChunkOff :plainChunkOff + copySize ])
438+ p = p [copySize :]
439+ off += int64 (copySize )
440+ n += copySize
441+ }
442+ if off == r .size {
443+ return n , io .EOF
444+ }
445+ return n , nil
446+ }
0 commit comments