@@ -18,7 +18,6 @@ You should have received a copy of the GNU General Public License
1818*/
1919
2020using System ;
21- using System . Collections . Generic ;
2221using System . IO ;
2322using System . Security . Cryptography ;
2423using System . Threading ;
@@ -27,6 +26,19 @@ namespace DnsServerCore.Dns.Security
2726{
2827 public class DnsCookieSecretManager
2928 {
29+ #region constants
30+
31+ private const int FileVersion = 1 ;
32+
33+ // Operational bounds; keep aligned with validator policy.
34+ private const int MinSecretLen = 16 ;
35+ private const int MaxSecretLen = 256 ;
36+
37+ // Default secret size (256-bit)
38+ private const int DefaultSecretLen = 32 ;
39+
40+ #endregion
41+
3042 #region variables
3143
3244 readonly string _secretFilePath ;
@@ -42,98 +54,113 @@ public class DnsCookieSecretManager
4254
4355 public DnsCookieSecretManager ( string secretFilePath )
4456 {
57+ if ( string . IsNullOrWhiteSpace ( secretFilePath ) )
58+ throw new ArgumentException ( "Secret file path must not be null or empty." , nameof ( secretFilePath ) ) ;
59+
4560 _secretFilePath = secretFilePath ;
46- Load ( ) ;
61+
62+ lock ( _lock )
63+ {
64+ LoadLocked ( ) ;
65+ }
4766 }
4867
4968 #endregion
5069
5170 #region private
5271
53- private void Load ( )
72+ private void LoadLocked ( )
5473 {
55- lock ( _lock )
74+ // Caller must hold _lock
75+ if ( ! File . Exists ( _secretFilePath ) )
5676 {
57- if ( File . Exists ( _secretFilePath ) )
58- {
59- try
60- {
61- byte [ ] data = File . ReadAllBytes ( _secretFilePath ) ;
62- using ( MemoryStream ms = new MemoryStream ( data ) )
63- using ( BinaryReader br = new BinaryReader ( ms ) )
64- {
65- int version = br . ReadInt32 ( ) ;
66- if ( version != 1 )
67- throw new InvalidDataException ( "Unsupported secret file version." ) ;
68-
69- _currentSecretCreated = new DateTime ( br . ReadInt64 ( ) , DateTimeKind . Utc ) ;
70-
71- int currentLen = br . ReadInt32 ( ) ;
72- if ( currentLen < 8 || currentLen > 256 )
73- throw new InvalidDataException ( "Invalid current secret length." ) ;
74-
75- _currentSecret = br . ReadBytes ( currentLen ) ;
76-
77- int previousLen = br . ReadInt32 ( ) ;
78- if ( previousLen < 8 || previousLen > 256 )
79- throw new InvalidDataException ( "Invalid previous secret length." ) ;
80-
81- _previousSecret = br . ReadBytes ( previousLen ) ;
82- }
83- }
84- catch
85- {
86- // If loading fails, generate new secrets
87- GenerateNewSecrets ( ) ;
88- }
89- }
90- else
77+ GenerateNewSecretsLocked ( ) ;
78+ return ;
79+ }
80+
81+ try
82+ {
83+ byte [ ] data = File . ReadAllBytes ( _secretFilePath ) ;
84+ using MemoryStream ms = new MemoryStream ( data , writable : false ) ;
85+ using BinaryReader br = new BinaryReader ( ms ) ;
86+
87+ int version = br . ReadInt32 ( ) ;
88+ if ( version != FileVersion )
89+ throw new InvalidDataException ( "Unsupported secret file version." ) ;
90+
91+ _currentSecretCreated = new DateTime ( br . ReadInt64 ( ) , DateTimeKind . Utc ) ;
92+
93+ int currentLen = br . ReadInt32 ( ) ;
94+ if ( currentLen < MinSecretLen || currentLen > MaxSecretLen )
95+ throw new InvalidDataException ( "Invalid current secret length." ) ;
96+
97+ byte [ ] current = br . ReadBytes ( currentLen ) ;
98+ if ( current . Length != currentLen )
99+ throw new EndOfStreamException ( "Unexpected end of secret file (current secret)." ) ;
100+
101+ int previousLen = br . ReadInt32 ( ) ;
102+ byte [ ] previous = null ;
103+
104+ if ( previousLen != 0 )
91105 {
92- GenerateNewSecrets ( ) ;
106+ if ( previousLen < MinSecretLen || previousLen > MaxSecretLen )
107+ throw new InvalidDataException ( "Invalid previous secret length." ) ;
108+
109+ previous = br . ReadBytes ( previousLen ) ;
110+ if ( previous . Length != previousLen )
111+ throw new EndOfStreamException ( "Unexpected end of secret file (previous secret)." ) ;
93112 }
113+
114+ _currentSecret = current ;
115+ _previousSecret = previous ;
116+ }
117+ catch
118+ {
119+ GenerateNewSecretsLocked ( ) ;
94120 }
95121 }
96122
97- private void Save ( )
123+ private void SaveLocked ( )
98124 {
99- lock ( _lock )
125+ // Caller must hold _lock
126+ if ( _currentSecret is null || _currentSecret . Length < MinSecretLen )
127+ throw new InvalidOperationException ( "Current secret is missing or too short." ) ;
128+
129+ using MemoryStream ms = new MemoryStream ( ) ;
130+ using ( BinaryWriter bw = new BinaryWriter ( ms ) )
100131 {
101- using ( MemoryStream ms = new MemoryStream ( ) )
132+ bw . Write ( FileVersion ) ;
133+ bw . Write ( _currentSecretCreated . Ticks ) ;
134+
135+ bw . Write ( _currentSecret . Length ) ;
136+ bw . Write ( _currentSecret ) ;
137+
138+ if ( _previousSecret is { Length : >= MinSecretLen and <= MaxSecretLen } )
139+ {
140+ bw . Write ( _previousSecret . Length ) ;
141+ bw . Write ( _previousSecret ) ;
142+ }
143+ else
102144 {
103- using ( BinaryWriter bw = new BinaryWriter ( ms ) )
104- {
105- bw . Write ( 1 ) ; // version
106- bw . Write ( _currentSecretCreated . Ticks ) ;
107-
108- bw . Write ( _currentSecret . Length ) ;
109- bw . Write ( _currentSecret ) ;
110-
111- if ( _previousSecret != null )
112- {
113- bw . Write ( _previousSecret . Length ) ;
114- bw . Write ( _previousSecret ) ;
115- }
116- else
117- {
118- bw . Write ( 0 ) ;
119- }
120- }
121-
122- string directory = Path . GetDirectoryName ( _secretFilePath ) ;
123- if ( ! string . IsNullOrEmpty ( directory ) && ! Directory . Exists ( directory ) )
124- Directory . CreateDirectory ( directory ) ;
125-
126- File . WriteAllBytes ( _secretFilePath , ms . ToArray ( ) ) ;
145+ bw . Write ( 0 ) ;
127146 }
128147 }
148+
149+ string directory = Path . GetDirectoryName ( _secretFilePath ) ;
150+ if ( ! string . IsNullOrEmpty ( directory ) && ! Directory . Exists ( directory ) )
151+ Directory . CreateDirectory ( directory ) ;
152+
153+ File . WriteAllBytes ( _secretFilePath , ms . ToArray ( ) ) ;
129154 }
130155
131- private void GenerateNewSecrets ( )
156+ private void GenerateNewSecretsLocked ( )
132157 {
133- _currentSecret = RandomNumberGenerator . GetBytes ( 32 ) ;
158+ // Caller must hold _lock
159+ _currentSecret = RandomNumberGenerator . GetBytes ( DefaultSecretLen ) ;
134160 _currentSecretCreated = DateTime . UtcNow ;
135161 _previousSecret = null ;
136- Save ( ) ;
162+
163+ SaveLocked ( ) ;
137164 }
138165
139166 #endregion
@@ -144,26 +171,28 @@ public void Rotate()
144171 {
145172 lock ( _lock )
146173 {
147- _previousSecret = _currentSecret != null ? ( byte [ ] ) _currentSecret . Clone ( ) : null ;
148- _currentSecret = RandomNumberGenerator . GetBytes ( 32 ) ;
174+ _previousSecret = _currentSecret is null ? null : ( byte [ ] ) _currentSecret . Clone ( ) ;
175+ _currentSecret = RandomNumberGenerator . GetBytes ( DefaultSecretLen ) ;
149176 _currentSecretCreated = DateTime . UtcNow ;
150- Save ( ) ;
177+
178+ SaveLocked ( ) ;
151179 }
152180 }
153181
154- public ReadOnlySpan < byte > GetCurrentSecret ( )
182+ // Returning spans here is unsafe once the lock is released; return a clone instead.
183+ public byte [ ] GetCurrentSecret ( )
155184 {
156185 lock ( _lock )
157186 {
158- return new ReadOnlySpan < byte > ( _currentSecret ) ;
187+ return _currentSecret is null ? null : ( byte [ ] ) _currentSecret . Clone ( ) ;
159188 }
160189 }
161190
162- public ReadOnlySpan < byte > GetPreviousSecret ( )
191+ public byte [ ] GetPreviousSecret ( )
163192 {
164193 lock ( _lock )
165194 {
166- return new ReadOnlySpan < byte > ( _previousSecret ) ;
195+ return _previousSecret is null ? null : ( byte [ ] ) _previousSecret . Clone ( ) ;
167196 }
168197 }
169198
0 commit comments