@@ -34,11 +34,12 @@ pub fn compute_admin_link(secret_code: &str, email: &str) -> String {
3434 hex:: encode ( hasher. finalize ( ) )
3535}
3636
37- /// Compute openhash = HMAC-SHA256(secret_code, "ucode:topic").
38- pub fn compute_openhash ( secret_code : & str , ucode : & str , topic : & str ) -> String {
37+ /// Compute openhash = HMAC-SHA256(secret_code, "ucode:topic:url").
38+ /// For open-tracking (no URL), pass `url = ""`.
39+ pub fn compute_openhash ( secret_code : & str , ucode : & str , topic : & str , url : & str ) -> String {
3940 let mut mac =
4041 HmacSha256 :: new_from_slice ( secret_code. as_bytes ( ) ) . expect ( "HMAC accepts any key length" ) ;
41- let message = format ! ( "{ucode}:{topic}" ) ;
42+ let message = format ! ( "{ucode}:{topic}:{url} " ) ;
4243 mac. update ( message. as_bytes ( ) ) ;
4344 hex:: encode ( mac. finalize ( ) . into_bytes ( ) )
4445}
@@ -54,8 +55,15 @@ pub fn verify_admin_link(provided: &str, expected: &str) -> bool {
5455}
5556
5657/// Verify openhash using constant-time comparison.
57- pub fn verify_openhash ( secret_code : & str , ucode : & str , topic : & str , provided : & str ) -> bool {
58- let expected = compute_openhash ( secret_code, ucode, topic) ;
58+ /// For open-tracking (no URL), pass `url = ""`.
59+ pub fn verify_openhash (
60+ secret_code : & str ,
61+ ucode : & str ,
62+ topic : & str ,
63+ url : & str ,
64+ provided : & str ,
65+ ) -> bool {
66+ let expected = compute_openhash ( secret_code, ucode, topic, url) ;
5967 verify_admin_link ( provided, & expected)
6068}
6169
@@ -116,27 +124,77 @@ mod tests {
116124
117125 #[ test]
118126 fn test_compute_openhash_deterministic ( ) {
119- let h1 = compute_openhash ( "secret" , "abc123" , "newsletter-01" ) ;
120- let h2 = compute_openhash ( "secret" , "abc123" , "newsletter-01" ) ;
127+ let h1 = compute_openhash ( "secret" , "abc123" , "newsletter-01" , "" ) ;
128+ let h2 = compute_openhash ( "secret" , "abc123" , "newsletter-01" , "" ) ;
121129 assert_eq ! ( h1, h2) ;
122130 }
123131
132+ #[ test]
133+ fn test_compute_openhash_url_changes_hash ( ) {
134+ let h1 = compute_openhash ( "secret" , "abc123" , "newsletter-01" , "" ) ;
135+ let h2 = compute_openhash ( "secret" , "abc123" , "newsletter-01" , "https://coscup.org" ) ;
136+ assert_ne ! ( h1, h2) ;
137+ }
138+
124139 #[ test]
125140 fn test_verify_openhash_correct ( ) {
126- let hash = compute_openhash ( "secret" , "abc123" , "newsletter-01" ) ;
127- assert ! ( verify_openhash( "secret" , "abc123" , "newsletter-01" , & hash) ) ;
141+ let hash = compute_openhash ( "secret" , "abc123" , "newsletter-01" , "" ) ;
142+ assert ! ( verify_openhash(
143+ "secret" ,
144+ "abc123" ,
145+ "newsletter-01" ,
146+ "" ,
147+ & hash
148+ ) ) ;
149+ }
150+
151+ #[ test]
152+ fn test_verify_openhash_correct_with_url ( ) {
153+ let url = "https://coscup.org/2025" ;
154+ let hash = compute_openhash ( "secret" , "abc123" , "newsletter-01" , url) ;
155+ assert ! ( verify_openhash(
156+ "secret" ,
157+ "abc123" ,
158+ "newsletter-01" ,
159+ url,
160+ & hash
161+ ) ) ;
162+ }
163+
164+ #[ test]
165+ fn test_verify_openhash_wrong_url ( ) {
166+ let hash = compute_openhash ( "secret" , "abc123" , "newsletter-01" , "https://coscup.org" ) ;
167+ assert ! ( !verify_openhash(
168+ "secret" ,
169+ "abc123" ,
170+ "newsletter-01" ,
171+ "https://evil.com" ,
172+ & hash
173+ ) ) ;
128174 }
129175
130176 #[ test]
131177 fn test_verify_openhash_wrong_topic ( ) {
132- let hash = compute_openhash ( "secret" , "abc123" , "newsletter-01" ) ;
133- assert ! ( !verify_openhash( "secret" , "abc123" , "newsletter-02" , & hash) ) ;
178+ let hash = compute_openhash ( "secret" , "abc123" , "newsletter-01" , "" ) ;
179+ assert ! ( !verify_openhash(
180+ "secret" ,
181+ "abc123" ,
182+ "newsletter-02" ,
183+ "" ,
184+ & hash
185+ ) ) ;
134186 }
135187
136188 #[ test]
137189 fn test_verify_openhash_wrong_secret ( ) {
138- let hash = compute_openhash ( "secret" , "abc123" , "newsletter-01" ) ;
139- assert ! ( !verify_openhash( "wrong" , "abc123" , "newsletter-01" , & hash) ) ;
190+ let hash = compute_openhash ( "secret" , "abc123" , "newsletter-01" , "" ) ;
191+ assert ! ( !verify_openhash(
192+ "wrong" ,
193+ "abc123" ,
194+ "newsletter-01" ,
195+ "" ,
196+ & hash
197+ ) ) ;
140198 }
141199
142200 #[ test]
0 commit comments