ctr.go raw
1 package crypto
2
3 import "golang.org/x/crypto/chacha20"
4
5 // CTR implements counter mode encryption using ChaCha20.
6 //
7 // A shared secret (Hamadryad) is hashed once to derive a 32-byte ChaCha20
8 // key. This is a one-time key-derivation step using SWIFFT's proven
9 // collision/preimage resistance -- the hash is NOT used as a PRF. All
10 // keystream generation is performed by ChaCha20.
11 //
12 // Random access: ChaCha20's SetCounter enables seeking in encrypted streams.
13
14 // BlockSize is the number of keystream bytes produced per ChaCha20 block counter.
15 const BlockSize = 64
16
17 // CTRStream holds the state for ChaCha20 stream encryption/decryption.
18 type CTRStream struct {
19 chachaKey [chacha20.KeySize]byte
20 chachaNonce [chacha20.NonceSize]byte
21 }
22
23 // newCipher creates a fresh ChaCha20 cipher from the stream's key/nonce.
24 func (s *CTRStream) newCipher() *chacha20.Cipher {
25 c, _ := chacha20.NewUnauthenticatedCipher(s.chachaKey[:], s.chachaNonce[:])
26 return c
27 }
28
29 // deriveChaCha20Key derives a 32-byte ChaCha20 key from Hamadryad key material.
30 func deriveChaCha20Key(keyMaterial Hamadryad) [chacha20.KeySize]byte {
31 keyHash := Hash(append([]byte("hamadryad-chacha20-key-v1"), keyMaterial[:]...))
32 var key [chacha20.KeySize]byte
33 copy(key[:], keyHash[:chacha20.KeySize])
34 return key
35 }
36
37 // deriveChaCha20Nonce derives a 12-byte ChaCha20 nonce from Hamadryad nonce material.
38 func deriveChaCha20Nonce(nonceMaterial Hamadryad) [chacha20.NonceSize]byte {
39 nonceHash := Hash(append([]byte("hamadryad-chacha20-nonce-v1"), nonceMaterial[:]...))
40 var nonce [chacha20.NonceSize]byte
41 copy(nonce[:], nonceHash[:chacha20.NonceSize])
42 return nonce
43 }
44
45 // NewCTRStreamFromSecret creates a CTR stream using a shared secret as the key.
46 // The nonce should be unique per message for a given secret.
47 func NewCTRStreamFromSecret(secret Hamadryad, nonce []byte) *CTRStream {
48 nonceHash := Hash(nonce)
49 return &CTRStream{
50 chachaKey: deriveChaCha20Key(secret),
51 chachaNonce: deriveChaCha20Nonce(nonceHash),
52 }
53 }
54
55 // Encrypt encrypts plaintext using the ChaCha20 keystream.
56 func (s *CTRStream) Encrypt(plaintext []byte) []byte {
57 return s.xor(plaintext)
58 }
59
60 // Decrypt decrypts ciphertext using the ChaCha20 keystream.
61 // Decryption is identical to encryption (XOR is self-inverse).
62 func (s *CTRStream) Decrypt(ciphertext []byte) []byte {
63 return s.xor(ciphertext)
64 }
65
66 // KeystreamBlock generates the keystream block for a given counter.
67 // Each call is independent -- blocks can be generated in any order.
68 func (s *CTRStream) KeystreamBlock(counter uint64) []byte {
69 c := s.newCipher()
70 c.SetCounter(uint32(counter))
71 block := make([]byte, BlockSize)
72 c.XORKeyStream(block, block) // XOR zeros = raw keystream
73 return block
74 }
75
76 // EncryptCTRAt encrypts a single block at the given byte offset.
77 // This enables random-access encryption of large streams.
78 func (s *CTRStream) EncryptCTRAt(plaintext []byte, offset uint64) []byte {
79 c := s.newCipher()
80 blockIdx := uint32(offset / uint64(BlockSize))
81 blockOff := int(offset % uint64(BlockSize))
82 c.SetCounter(blockIdx)
83
84 // Discard partial block prefix to reach the target offset.
85 if blockOff > 0 {
86 discard := make([]byte, blockOff)
87 c.XORKeyStream(discard, discard)
88 }
89
90 result := make([]byte, len(plaintext))
91 c.XORKeyStream(result, plaintext)
92 return result
93 }
94
95 // xor XORs data with the ChaCha20 keystream.
96 func (s *CTRStream) xor(data []byte) []byte {
97 c := s.newCipher()
98 result := make([]byte, len(data))
99 c.XORKeyStream(result, data)
100 return result
101 }
102