do_challenge_address.sql (7915B)
1 -- 2 -- This file is part of TALER 3 -- Copyright (C) 2024 Taler Systems SA 4 -- 5 -- TALER is free software; you can redistribute it and/or modify it under the 6 -- terms of the GNU General Public License as published by the Free Software 7 -- Foundation; either version 3, or (at your option) any later version. 8 -- 9 -- TALER is distributed in the hope that it will be useful, but WITHOUT ANY 10 -- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 -- A PARTICULAR PURPOSE. See the GNU General Public License for more details. 12 -- 13 -- You should have received a copy of the GNU General Public License along with 14 -- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 -- 16 17 18 DROP FUNCTION IF EXISTS challenger_do_challenge_set_address_and_pin; 19 CREATE FUNCTION challenger_do_challenge_set_address_and_pin ( 20 IN in_nonce BYTEA, 21 IN in_address TEXT, 22 -- Newest last_tx_time for which a (re)transmission is still due, that is 23 -- 'now - retransmission_frequency'. Computed by the caller because 24 -- last_tx_time is only known here; see do_challenge_address.c. 25 IN in_retransmit_cutoff INT8, 26 IN in_now INT8, 27 IN in_tan INT4, 28 OUT out_not_found BOOLEAN, 29 OUT out_last_tx_time INT8, 30 OUT out_last_pin INT4, 31 OUT out_state TEXT, 32 OUT out_pin_transmit BOOLEAN, 33 OUT out_auth_attempts_left INT4, 34 -- How many TAN transmissions are left *after* this call? Note that 35 -- this is a different budget from out_auth_attempts_left, which counts 36 -- the guesses the user has on the current TAN. 37 OUT out_pin_transmissions_left INT4, 38 OUT out_client_redirect_uri TEXT, 39 OUT out_address_refused BOOLEAN, 40 OUT out_solved BOOLEAN, 41 -- TRUE if the validation failed permanently: no address change, TAN 42 -- transmission or TAN attempt is left. 43 OUT out_failed BOOLEAN) 44 LANGUAGE plpgsql 45 AS $$ 46 DECLARE 47 my_status RECORD; 48 my_do_update BOOL; 49 my_address_differs BOOL; 50 BEGIN 51 52 my_do_update = FALSE; 53 54 SELECT address 55 ,address_attempts_left 56 ,pin_transmissions_left 57 ,last_tx_time 58 ,client_redirect_uri 59 ,last_pin 60 ,pending_pin 61 ,auth_attempts_left 62 ,client_state 63 ,failed 64 INTO my_status 65 FROM validations 66 WHERE nonce=in_nonce 67 AND expiration_time > in_now 68 FOR UPDATE; 69 70 IF NOT FOUND 71 THEN 72 out_not_found=TRUE; 73 out_last_tx_time=0; 74 out_last_pin=NULL; 75 out_pin_transmit=FALSE; 76 out_auth_attempts_left=0; 77 out_pin_transmissions_left=0; 78 out_client_redirect_uri=NULL; 79 out_address_refused=TRUE; 80 out_solved=FALSE; 81 out_failed=FALSE; 82 out_state=NULL; 83 RETURN; 84 END IF; 85 out_not_found=FALSE; 86 out_failed=FALSE; 87 out_last_tx_time=my_status.last_tx_time; 88 out_last_pin=my_status.last_pin; 89 out_pin_transmit=FALSE; 90 out_auth_attempts_left=my_status.auth_attempts_left; 91 out_pin_transmissions_left=my_status.pin_transmissions_left; 92 out_state=my_status.client_state; 93 out_client_redirect_uri=my_status.client_redirect_uri; 94 95 IF ( 0 > my_status.auth_attempts_left ) -- this challenge is solved 96 THEN 97 out_address_refused=TRUE; 98 out_solved=TRUE; 99 out_auth_attempts_left=0; 100 RETURN; 101 END IF; 102 out_solved=FALSE; 103 104 -- Once out of options, nothing this call could do helps: an address change 105 -- is refused (address_attempts_left is 0, or the C layer refused to change 106 -- a read-only address before calling us) and no TAN may be transmitted. 107 -- A validation can get here without a /solve, e.g. if the helper failed on 108 -- the last transmission after the guesses on the previous TAN were spent. 109 IF ( my_status.failed OR 110 ( (0 = my_status.auth_attempts_left) AND 111 (0 = my_status.pin_transmissions_left) AND 112 ( (0 = my_status.address_attempts_left) OR 113 COALESCE (my_status.address::JSONB->'read_only' 114 = 'true'::JSONB, FALSE) ) ) ) 115 THEN 116 IF NOT my_status.failed 117 THEN 118 UPDATE validations 119 SET failed=TRUE 120 WHERE nonce=in_nonce; 121 END IF; 122 out_failed=TRUE; 123 out_address_refused=TRUE; 124 out_last_pin=NULL; 125 out_auth_attempts_left=0; 126 RETURN; 127 END IF; 128 129 -- Two addresses are the same address if they are the same JSON *value*. 130 -- Comparing the raw text instead would make a purely cosmetic difference -- 131 -- a different field order, redundant whitespace -- count as a different 132 -- address, with two consequences. Firstly, the daemon itself re-appends 133 -- 'read_only' to the address as the *last* field (see 134 -- challenger-httpd_challenge.c), so an unchanged address can come back here 135 -- with its fields in a different order and silently cost an honest user one 136 -- of their address attempts. Secondly, the budget is deliberately 137 -- 'address_attempts_left' addresses times 'pin_transmissions_left' TANs 138 -- each; if cosmetic variants counted as distinct addresses, all of those 139 -- messages could be aimed at a single recipient. Comparing as JSONB also 140 -- makes this agree with the C layer, which compares addresses with 141 -- json_equal() in addr_equal(). 142 my_address_differs = ( (my_status.address IS NOT NULL) AND 143 (in_address::JSONB IS DISTINCT FROM 144 my_status.address::JSONB) ); 145 146 IF ( (0 = my_status.address_attempts_left) AND 147 my_address_differs ) 148 THEN 149 out_address_refused=TRUE; 150 out_last_pin=NULL; 151 RETURN; 152 END IF; 153 out_address_refused=FALSE; 154 155 IF ( my_address_differs OR 156 (my_status.address IS NULL) ) 157 THEN 158 -- We are changing the address, update counters. Refilling 159 -- 'pin_transmissions_left' and clearing the retransmission cooldown is 160 -- intentional: the user gets a fixed number of addresses and, for each 161 -- address, a fixed number of TAN transmissions, so that a user who 162 -- mistyped their address does not have to wait out the cooldown of a 163 -- message that went to somebody else. 164 my_status.address_attempts_left 165 = GREATEST(0,my_status.address_attempts_left - 1); 166 -- Store the canonical (JSONB) rendering, so that what is on file does not 167 -- depend on the field order the client happened to use. 168 my_status.address = in_address::JSONB::TEXT; 169 my_status.pin_transmissions_left = 3; 170 my_status.last_tx_time = 0; 171 -- The TAN generated below is now merely 'pending' until the helper 172 -- confirms the transmission, so -- unlike before this patch -- this call 173 -- no longer necessarily overwrites 'last_pin'. The TAN on file went to 174 -- the *previous* address, so it must be dropped here: if the helper then 175 -- fails, the user must be left without a usable TAN rather than with one 176 -- that attests an address it was never sent to. 177 my_status.last_pin = NULL; 178 my_status.pending_pin = NULL; 179 my_status.auth_attempts_left = 0; 180 out_last_pin = NULL; 181 out_auth_attempts_left = 0; 182 my_do_update=TRUE; 183 END IF; 184 185 IF ( (my_status.pin_transmissions_left > 0) AND 186 (my_status.last_tx_time <= in_retransmit_cutoff) ) 187 THEN 188 -- Enough time has passed since the last transmission, so we are changing 189 -- the TAN, update counters. The new TAN is only stored as 'pending_pin': 190 -- it is promoted to 'last_pin' (and 'auth_attempts_left' reset) by 191 -- CHALLENGERDB_do_challenge_address_confirm_pin() once the AUTH_COMMAND 192 -- helper confirmed the transmission. Until then the TAN the user may 193 -- already hold from an earlier transmission stays valid. 194 my_status.pin_transmissions_left = my_status.pin_transmissions_left - 1; 195 my_status.pending_pin = in_tan; 196 my_status.last_tx_time = in_now; 197 out_pin_transmissions_left = my_status.pin_transmissions_left; 198 out_auth_attempts_left = 3; 199 out_pin_transmit=TRUE; 200 out_last_pin = in_tan; 201 out_last_tx_time = in_now; 202 my_do_update=TRUE; 203 END IF; 204 205 IF my_do_update 206 THEN 207 UPDATE validations SET 208 address=my_status.address 209 ,address_attempts_left=my_status.address_attempts_left 210 ,pin_transmissions_left=my_status.pin_transmissions_left 211 ,last_tx_time=my_status.last_tx_time 212 ,last_pin=my_status.last_pin 213 ,pending_pin=my_status.pending_pin 214 ,auth_attempts_left=my_status.auth_attempts_left 215 WHERE nonce=in_nonce; 216 END IF; 217 218 RETURN; 219 220 END $$;