-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathserver.py
More file actions
1157 lines (1024 loc) · 69.8 KB
/
server.py
File metadata and controls
1157 lines (1024 loc) · 69.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
'''
Overview
This file is the HTTP layer which declares the functions that serve the routes for interacting
with the Session Pro Backend. These routes are registered onto a Flask application which enable
the endpoints for the server.
The role of this layer is to intercept and sanitize the HTTP request, extracting the JSON into
valid, strongly typed (to Python's best ability) types that can be passed into the backend.
The backend is responsible for further validation of the request such as signature verification
and consistency against the state of the DB. If successful the result is returned back to this
layer and piped back to the user in the HTTP response.
API
All response endpoints follow the basic structure for success and failure respectively:
{ "status": 0, "result": { "version": 0, <content...> }} // On success
{ "status": 1, "errors": [ "1st reason for error", "2nd reason for error", ... ]} // On failure
Which means that calling code should conditionally handle a root level `result` or `errors` type
payload based on the status. `0` for success and non-`0` for failures.
All routes accept v4 onion requests under the endpoint /oxen/v4/lsrpc
/add_pro_payment
Description
Register a new payment identified by the payment details specified in the 'payment_tx' (e.g.:
Google Play Store and the payment token) to the Session Pro back-end.
The master public key `master_pkey` should be the deterministically derived Ed25519 public key
from the user's Session Account seed. The rotating public key `rotating_pkey` should be an
independent Ed25519 key that will be authorised to use the proof.
The embedded `master_sig` and `rotating_sig` signature must sign over a 32 byte hash of the
request components (in little endian) for example:
google_hash = blake2b32(person='ProAddPayment___', version || master_pkey || rotating_pkey || payment_tx.provider || payment_tx.google_payment_token || payment_tx.google_order_id)
apple_hash = blake2b32(person='ProAddPayment___', version || master_pkey || rotating_pkey || payment_tx.provider || payment_tx.apple_tx_id)
This request will fail if the Session Pro backend has not witnessed the equivalent payment
independently from the storefront that the payment originally came from.
The generated proof is signed by the Session Pro Backend. See `/generate_pro_proof` comments
for details on how to verify the proofs were generated by an authoritative Session Pro
Backend.
Request
version: 1 byte, current version of the request which should be 0
master_pkey: 32 byte Ed25519 master Session Pro public key derived deterministically from
the Session Account seed in hex to get pro status for
rotating_pkey: 32 byte Ed25519 public key to pair to the pro proof in hex
payment_tx: Object containing fields about the purchase from the payment provider to
register for a Session Pro subscription.
provider: 1 byte integer representing the platform that the payment to be
registered is coming from with the following mapping:
1 => Google Play Store
2 => Apple iOS App Store
apple_tx_id: When provider is set to Apple iOS App Store, set this field to the
transaction ID string.
google_payment_token: When provider is set to the Google Play Store, set this field to the
purchase token string.
google_order_id: When provider is set to the Google Play Store, set this field to the
order id string.
master_sig: 64 byte signature over the hash of the contents of the request proving that the
user knows the secret component to the `master_pkey` and hence the caller is
authorised to pair a new `rotating_pkey` to this payment
rotating_sig: 64 byte signature over the contents of the request proving that the user knows
the secret component to the `rotating_pkey`
Response
result: Result object with the pro proof, only set if status was success otherwise there will
be an error array as aforementioned.
version: 1 byte version value from the request
expiry_unix_ts_ms: 8 byte UNIX time-stamp of when the proof will expire
gen_index_hash: 32 byte hash of the internal generation index that has been allocated to
the user. This hash is the unique identifier for all Session Pro Proofs
generated for a given payment.
rotating_pkey: 32 byte Ed25519 public key authorised to use the proof
sig: 64 byte signature over the proof, signed by the Session Pro backend allowing
third-parties to verify that the proof was generated by a authoritative
back-end.
status: 1 byte integer corresponding to the status code of the request with the following
mapping from 'AddProPaymentStatus':
0 => Success: Payment was claimed and the pro proof is in the result
object
1 => Error: Backend encountered an error when attempting to claim the
payment
2 => Parse Error: Request specified the incorrect fields or fields with
unusable values
100 => Already Redeemed: Payment is already claimed
101 => Unknown Payment: Payment transaction attempted to claim a payment that the
backend does not have. Either the payment doesn't exist or
the backend has not witnessed the payment from the provider
yet.
Example
Request
{
"version": 0,
"master_pkey": "162c30675ecc72ad17ef57e749a54284812cc178b1d2f31cfb3260f1f7594dc5",
"rotating_pkey": "ecd0e9c371b5e1d9e116ba4d29b057e458c8b4bca40b5b3fea1cd5d5e89ae7b7",
"master_sig": "63204c239ab4a8b2ec06b591b7a845bab13c5189389a5af216d5f97d48c71b9ed4378bb576da029774775d727c54bd48372f6bd7e565f90d6138b94e803caf06",
"rotating_sig": "49fa7e5ea564a1e5428acadfa68d602fbc35aac6ac4fcc9022f54cb2819242fad49f375cf11606aeab9d532583480d215dfb18fcac8671899a4e645d32b00803",
"payment_tx": {
"provider": 1,
"google_payment_token": "b228c0144d1368541410693c82bbceb1",
"google_order_id": "a7ac920177ee9b1fc524c80d377de1ec"
}
}
Response
{
"result": {
"expiry_unix_ts_ms": 1762407280000,
"gen_index_hash": "2caeefdd95a0ce0dfbdbdeca987e7cdd7cb40fb40de55282931740c56ca40245",
"rotating_pkey": "ecd0e9c371b5e1d9e116ba4d29b057e458c8b4bca40b5b3fea1cd5d5e89ae7b7",
"sig": "51d2ea19a4e26ea4181214ce8f72ea1f8c9b3b53a911399a3a63713272211aec481c40d6ab65f91f71ef093bbda608f037aafba73482a304db6fe30f1806130b",
"version": 0
},
"status": 0
}
/generate_pro_proof
Description
Pair a new `rotating_pkey` to a pre-existing Session Pro payment, generating a new proof that
can be attached to messages to enable entitlement to Pro features on the Session Protocol. The
`master_pkey` public key must currently have an active subscription/payment associated with it
for this request to succeed.
The embedded `master_sig` and `rotating_sig` signature must sign over a 32 byte hash of the
request components (in little endian):
hash = blake2b32(person='ProGenerateProof', version || master_pkey || rotating_pkey || unix_ts_ms)
Once the response has been received, the caller should store the proof offline and embed it
into their messages on the Session Protocol, signing the message with their rotating secret
key for other users to validate the proof and receive entitlement to Session Pro features.
The generated proof is signed by the Session Pro Backend. The public keys of the backend will
be published to allow third parties to authorise the validity of a proof's origin. The
signature in the response signs over a 32 byte hash of the following response components (in
little endian):
hash = blake2b32(person='ProProof________', version || gen_index_hash || rotating_pkey || expiry_unix_ts_ms)
Request
version: 1 byte, current version of the request which should be 0
master_pkey: 32 byte Ed25519 master Session Pro public key derived deterministically from
the Session Account seed in hex to get pro status for
rotating_pkey: 32 byte Ed25519 public key to pair to the pro proof in hex
unix_ts_ms: 8 byte current UNIX timestamp
master_sig: 64 byte signature over the hash of the contents of the request proving that the
user knows the secret component to the `master_pkey` and hence the caller is
authorised to pair a new `rotating_pkey` to the payment associated with the
`master_pkey`.
rotating_sig: 64 byte signature over the contents of the request proving that the user knows
the secret component to the `rotating_pkey`.
Response
status: Response status, either RESPONSE_SUCCESS, RESPONSE_PARSE_ERROR or
RESPONSE_GENERIC_ERROR
result:
version: 1 byte version value from the request
expiry_unix_ts_ms: 8 byte UNIX timestamp of when the proof will expire
gen_index_hash: 32 byte hash of the internal generation index that has been allocated to
the user.
rotating_pkey: 32 byte Ed25519 public key authorised to use the proof
sig: 64 byte signature over the proof, signed by the Session Pro backend
allowing third-parties to verify that the proof was generated by a
authoritative backend.
Examples
Request
{
"version": 0,
"master_pkey": "2a87bf679678fe7ccad36ae081de58ee327f1a6706d1f2b2ecda52219b7ee8bf",
"rotating_pkey": "67917f7507c58880c50e249afecb2fe4a236d422c7e05b04d4fbf46e30c965d5",
"unix_ts_ms": 1755648412000,
"master_sig": "76e02d201fad147a318aa798196bf9880bf4425f529e24bb25af34a4181365ef7591ae066b31aded05e3b67370892f381910fcaf3c2ffb5be13cca389a572108",
"rotating_sig": "74ffec4d91caf777d439f2f34a1c7375ff746ae798181bf357050f7848fd8b7f100a933b61e15f13cacdbd6028b37bc1f2a4d8888b2b04a6e5d7e51b38dd360e"
}
Response
{
"result": {
"expiry_unix_ts_ms": 1758412800000,
"gen_index_hash": "084563482babfdf1acda66fcef7c70ad835e148ab98f26371ce9e4abef6104d7",
"rotating_pkey": "67917f7507c58880c50e249afecb2fe4a236d422c7e05b04d4fbf46e30c965d5",
"sig": "a1ea79c2a274afc0a61e5946976297b42e1dcfdbde29f007c8fe43d2e616fc7e5db5865d05212e392a6395fabe1ed69f976fb19c25f4640df5b89a5870739e0e",
"version": 0
},
"status": 0
}
/get_pro_revocations
Description
Retrieve the list of revoked Session Pro Proofs. Proofs are signed can be validated offline in
perpetuity until expiry. There are situations where a current circulating and valid proof can
be invalidated (for example a user has refunded their subscription). The Session Pro Backend
maintains the list of proofs that callers can retrieve to reject proofs that circulating on
the network.
This endpoint accepts a `ticket` which represents the current iteration of the revocation
list. The Session Pro Backend increments the ticket monotonically with each change to the
revocation list. The caller submits their latest known `ticket` (which initially will be 0)
and in the response the latest `ticket` known by the backend will be returned.
By having the caller cache the ticket and reuse it in subsequent requests, the backend will
only return the revocation list contents if the user's `ticket` is different from the
backend's ticket.
A revocation identifies a Session Pro proof using the `gen_index_hash` of the proof. This
`gen_index_hash` is shared across all proofs for a user that were generated using the same
payment, in other words there can be more than one proof circulating with this
`gen_index_hash`. Callers must take care to reject all proofs they witness that match the
`gen_index_hash` irrespective of any other common information in the proof.
Note that expired proofs do not get revoked and will not show up in this list. It's the
caller's responsibility to reject proofs that have expired by checking the expiry timestamp.
Hence this endpoint is recommended to be called every hour from the caller's startup time as
revocations are only created in exceptional circumstances (such as refunds or protocol
mandated revocations, e.g.: rare).
Request
version: 1 byte, current version of the request which should be 0
ticket: 4 byte monotonic integer that represents the current iteration of the revocation list
held by the caller. Initially callers will set this to 0 if they do not know the
latest ticket. In subsequent requests the latest known `ticket` should be passed in
so that the backend only returns the updated revocation list if the contents of said
list has changed.
Response
status: Response status, either RESPONSE_SUCCESS, RESPONSE_PARSE_ERROR or
RESPONSE_GENERIC_ERROR
result:
ticket: 4 byte integer of the latest ticket for the current revocation list of the Session
Pro backend. If this value is the same as the request's `ticket` then the list will
be empty as there are no changes to the revocation list.
items: Array of revocations, can be empty if there are no revocations or the request ticket
is the latest ticket managed by the backend.
expiry_unix_ts_ms: 8 byte UNIX timestamp indicating when the Session Pro Proof identified
by its `gen_index_hash` should be rejected until.
gen_index_hash: 32 byte hash of the Session Pro proof that has been revoked.
effective_unix_ts_ms: 8 byte UNIX timestamp indicating when the revocation becomes effective
(i.e., clients should start rejecting proofs at this time).
retry_in_s: 4 byte integer of the recommended time in seconds that the client should wait to
send the request for the pro-revocation list again to avoid being throttled.
Examples
Request
{ "version": 0, "ticket": 0 }
Response
{
"result": {
"items": [
{
"expiry_unix_ts_ms": 1758412800000,
"gen_index_hash": "3ab824a62d2b6004449d44962383294a5e6e833d6ed491930fbba726a2569c68",
"effective_unix_ts_ms": 1758326400000
}
],
"ticket": 1,
"retry_in_s": 86400,
"version": 0
},
"status": 0
}
/get_pro_details
Description
Retrieve the list of current and historical payments associated with the Session Pro master
public key. The returned list is in descending order from the date that the payment was
registered (e.g.: newest payment to oldest).
This request is paginated, initially the caller should pass the 0th page. The response will
have the total number of pages at which the caller can query more pages if there are any to
retrieve more payments.
The embedded `master_sig` signature must sign over the 32 byte hash of the requests contents
(in little endian):
hash = blake2b32(person='ProGetProDetReq_', version || master_pkey || unix_ts_ms || count)
TODO: In future we plan to prune payment history after some legally required threshold such as
a year.
TODO: We should paginate the count or provide feedback to the user how many payments they have
so they know how many entries to request. For now we just return the number of payments they
have
Request
version: 1 byte, current version of the request which should be 0
master_pkey: 32 byte Ed25519 master Session Pro public key derived deterministically from
the Session Account seed in hex to get pro status for
master_sig: 64 byte signature over the hash of the contents of the request proving that the
user knows the secret component to the `master_pkey` and hence the caller is
authorised to get pro status for this key.
unix_ts_ms: 8 byte UNIX timestamp of the current time.
count: 4 byte integer indicating up to how many payments can be populated in the `items`
array. `items` is capped to the actual number of payments available for
`master_pkey`.
Response
status: Response status, either RESPONSE_SUCCESS, RESPONSE_PARSE_ERROR or
RESPONSE_GENERIC_ERROR
result:
status: 1 byte integer describing the current Session Pro entitlement of the
associated key with the following mapping:
0 => Never Been Pro
User has never purchased/redeemed a Session Pro payment for the
associated master public key. Accordingly, the items array will be
empty.
1 => Active
User has a Session Pro payment that is actively being consumed for
entitlement to Session Pro features.
2 => Expired
User had Session Pro payment(s) that were fully consumed
previously and currently don't have an active payment and hence
entitlement to Session Pro features.
auto_renewing: 1 byte boolean indicating if the latest pro subscription (if active)
is set to auto-renew at the marked expiry time.
expiry_unix_ts_ms: 8 byte UNIX timestamp indicating the latest timestamp to which a user is
allowed to request a Session Pro Proof from the backend. This timestamp
is inclusive of the grace period a user may be allocated if they have an
auto- renewing subscription. This expiry value is roughly calculated as
the `max(subscription expiry + maybe grace period)` timestamp from their
list of payments that are active.
Application developers should prefer using this value to determine the
when a user's entitlement to Session Pro expires because selecting this
then defers the responsibility of choosing the best/most relevant active
payment associated with an account to the server which is the
authoritative source of truth.
refund_requested_unix_ts_ms: 8 byte UNIX timestamp indicating if the user has requested a
refund for their latest subscription that would be otherwise be
expiring at the payment associated with the
'expiring_unix_ts_ms'. This value is set to 0 if no refund has
been initiated.
grace_period_duration_ms: 8 byte duration integer indicating the grace period duration
indicating the amount of time the payment platform will attempt
to auto-renew the subscription after it has expired. Clients can
continue to request a proof for users during the grace period
that expires at the end of the period. The grace period is
included into the expiry timestamp thus the timestamp at which
auto-renewing of a subscription starts can be calculated by
`expiry_unix_ts_ms - grace_duration_ms` and that `auto_renewing`
is true. Note: on some platforms, the grace period is not known
until the user enters the grace period (such as Google) and as
such this value may be set at different value whilst
`auto_renewing` is true before being updated to its final value.
error_report: 1 byte integer error code where any non-zero value indicates that the
Session Pro Backend encountered an error book-keeping Session Pro for the
user. Their Session Pro status may be out-of-date, hence if this value is
non-zero, implementing clients can optionally display a warning or prompt
that the user should contact support for investigation.
payments_total: 4 byte integer indicating the total amount of payments the backend has for
the user, after, pruning. This value may be greater than the length of
items if you requested less than the user has or if new payments have been
made since the request.
items: Array of payments associated with `master_pkey`. Payments are returned in
descending order by the payment date
status: 1 byte integer describing the status of the consumption of the
payment for Session Pro with the following mapping:
2 => Redeemed
Payment was recognised by the backend and is being used
for Session Pro entitlement.
3 => Expired
Session Pro entitlement has expired and is no longer
being provided for this payment
4 => Revoked
User has successfully refunded/or had their payment
revoked and Session Pro entitlement is no longer
available
Always check the status before interpreting the fields. It's
possible to transition from revoked -> redeemed for example if
the payment provider cancels a refund in which case the revoked
timestamp is set but the payment is actually being actively
consumed.
plan: 1 byte integer indicating the Session Pro plan that was
purchased with the following mapping:
1 => 1 Month
2 => 3 Months
3 => 12 Months
payment_provider: 1 byte integer representing the platform that the payment to be
registered is coming from with the following mapping:
1 => Google Play Store
2 => Apple iOS App Store
3 => Rangeproof
auto_renewing: 1 byte boolean representing if the user had auto-renewing
enabled to repeat this payment. It additionally indicates that
the user is to be granted the grace period marked on the
payment.
unredeemed_unix_ts_ms: 8 byte UNIX timestamp indicating when the payment was executed.
redeemed_unix_ts_ms: 8 byte UNIX timestamp indicating when the payment was
registered. This timestamp is rounded up to the next day
boundary from the actual registration date.
expiry_unix_ts_ms: 8 byte UNIX timestamp indicating when the entitlement of Session
Pro is due to expire. Note this is _not_ inclusive of grace
unlike the expiry timestamp in the top-level result object.
grace_period_duration_ms: 8 byte duration integer indicating how long the subscription's
grace period is. Set to 0 if auto-renewing is disabled.
platform_refund_expiry_unix_ts_ms: 8 byte unix timestamp indicating when the payment will
no longer be eligible for a refund via its purchase
platform.
revoked_unix_ts_ms: 8 byte UNIX timestamp indicating when the payment was
revoked. 0 if it never revoked.
google_payment_token: When payment provider is Google Play Store, a string which is
set to the platform-specific purchase token for the
subscription.
google_order_id: When payment provider is Google Play Store, a string which is
set to the platform-specific order ID for the subscription.
apple_original_tx_id: When payment provider is Apple iOS App Store, a string which
is set to the platform-specific original transaction ID for
the subscription.
apple_tx_id: When payment provider is Apple iOS App Store, a string which
is set to the platform-specific transaction ID for the
subscription.
apple_web_line_order_id: When payment provider is Apple iOS App Store, a string which
is set to the platform-specific transaction web line order ID
for the subscription.
rangeproof_order_id: When payment provider is Rangeproof, a string which is set to
the platform-specific order ID for the subscription.
refund_requested_unix_ts_ms: 8 byte UNIX timestamp indicating if the user has requested a
refund for this payment. This value is set to 0 if no refund
has been initiated. Setting the refund request value for
a payment is optional and platforms must call the set refund
request endpoint if they wish to set this value.
Examples
Request
{
"version": 0,
"master_pkey": "8ddc57b457fca85d2184813ea18a048f64a35ab0e693d4a0a3e4f8ee87ff3360",
"master_sig": "37495dfab72772ebf4e4bf213b0a1c46e8e044ef3e4360ff8ef04ee8a7daf2178a716447de6f938d0e7865be31735fb2db2d1213dc35c02dfe253aac77fb2a0d",
"unix_ts_ms": 1755653705,
"count": 10000,
}
Response
{
"result": {
"items": [
{
"status": 2,
"plan": 1,
"payment_provider": 1,
"auto_renewing": 1,
"unredeemed_unix_ts_ms": 1759190400000,
"redeemed_unix_ts_ms": 1759190400000,
"expiry_unix_ts_ms": 1761718134941,
"grace_period_duration_ms": 0,
"platform_refund_expiry_unix_ts_ms": 1761718134941,
"revoked_unix_ts_ms": 0,
"google_payment_token": "ad8b67960eb91e8e2c0a4e8f191ea77b5ad593508b52ecc36c69c059cab39397fbf1e96142fa7fbcc7391cc3369ad110e3f9cbfccef284a925dcd470a4670aec",
"google_order_id": "993f7d1bbcf4dfda482a8bce4f2b62acfc8c2d3d06b6512dfc981738ddf85562490b016f27b07a17c080c0765ada43f2e4c0618196f667e1174d1b3d67752b86",
"refund_requested_unix_ts_ms": 0,
}
],
"auto_renewing": 1,
"expiry_unix_ts_ms": 1761782400000,
"grace_period_duration_ms": 60000,
"refund_requested_unix_ts_ms": 0,
"payments_total": 1,
"status": 1,
"error_report": 0,
"version": 0
},
"status": 0
}
/set_payment_refund_requested
Description
Mark a payment as having initiated a refund request (or unmark it by setting it 'off') so that
other platforms can synchronise the refund status of a particular payment. This feature is
opt-in by the originating device because in some situations, it's possible to initiate a
refund out-of-band from the application itself so it's not as easy to determine if a
subscription is currently being considered for a refund.
A timestamp of '0' signifies removing the refund request status from the payment and a
non-zero means that a refund request is underway. Currently this feature is only taken
advantage of by iOS.
iOS:
This platform must manually call this endpoint when a refund is initiated through the native
refund sheet to mark the payment for the benefit of other platforms. If the refund is denied
the platform does _not_ need to unmark the refund request as the backend will receive a
notification from Apple informing us that it was denied and it will attempt to unmark the
payment's refund request status on the user's behalf, setting the timestamp to '0'.
If a refund is successful, the payment will transition into a 'revoked' status as well as add
the payment to the revocation list. Platforms do not need to unmark the refund request status
of the payment. They must however take care to respect the user's Pro entitlement (revoked,
active, ... e.t.c) before displaying anything related to a refund request.
A refund request marked payment must only show refund related data if the payment is redeemed.
In all other cases, the payment status takes precedence over the refund request value.
The embedded `master_sig` signature must sign over the 32 byte hash of the requests contents
(in little endian):
google_hash = blake2b32(person='ProSetRefundReq_', version || master_pkey || unix_ts_ms || refund_requested_unix_ts_ms || payment_tx.provider || payment_tx.google_payment_token || payment_tx.google_order_id)
apple_hash = blake2b32(person='ProSetRefundReq_', version || master_pkey || unix_ts_ms || refund_requested_unix_ts_ms || payment_tx.provider || payment_tx.apple_tx_id)
Request
version: 1 byte, current version of the request which should be 0
master_pkey: 32 byte Ed25519 master Session Pro public key derived
deterministically from the Session Account seed in hex to get pro
status for
master_sig: 64 byte signature over the hash of the contents of the request
proving that the user knows the secret component to the
`master_pkey` and hence the caller is authorised to set the
refund request status of the payment.
unix_ts_ms: 8 byte UNIX timestamp of the current time.
refund_requested_unix_ts_ms: 8 byte UNIX timestamp of the timestamp to set as the timestamp
that a refund request was initiated at
payment_tx: Object containing fields about the purchase from the payment
provider to set the refund request on.
provider: 1 byte integer representing the platform that the payment to be
registered is coming from with the following mapping:
1 => Google Play Store
2 => Apple iOS App Store
apple_tx_id: When provider is set to Apple iOS App Store, set this field to
the transaction ID string.
google_payment_token: When provider is set to the Google Play Store, set this field to
the purchase token string.
google_order_id: When provider is set to the Google Play Store, set this field to
the order id string.
Response
status: Response status, either RESPONSE_SUCCESS, RESPONSE_PARSE_ERROR or
RESPONSE_GENERIC_ERROR
result:
version: 1 byte version value from the request
updated: True if a payment was found matching the given payment information and that the
refund request unix timestamp was set
Examples
Request
{
"version": 0,
"master_pkey": "8ddc57b457fca85d2184813ea18a048f64a35ab0e693d4a0a3e4f8ee87ff3360",
"master_sig": "37495dfab72772ebf4e4bf213b0a1c46e8e044ef3e4360ff8ef04ee8a7daf2178a716447de6f938d0e7865be31735fb2db2d1213dc35c02dfe253aac77fb2a0d",
"unix_ts_ms": 1755653705,
"refund_requested_unix_ts_ms": 1755653705,
"payment_tx": {
"provider": 1,
"google_payment_token": "b228c0144d1368541410693c82bbceb1",
"google_order_id": "a7ac920177ee9b1fc524c80d377de1ec"
}
}
Response
{
"result": {
"version": 0
"updated": true,
},
"status": 0
}
'''
import collections.abc
import contextlib
import dataclasses
import enum
import flask
import hashlib
import json
import nacl.bindings
import nacl.public
import nacl.signing
import sqlalchemy
import time
import typing
import logging
import db
import base
import backend
from vendor import onion_req
@dataclasses.dataclass
class GetJSONFromFlaskRequest:
json: dict[str, base.JSONValue] = dataclasses.field(default_factory=dict)
err_msg: str = ''
class UserProStatus(enum.IntEnum):
NeverBeenPro = 0
Active = 1
Expired = 2
# Keys stored in the flask app config dictionary that can be retrieved within
# a request to get the path to the SQLite DB to load and use for that request.
FLASK_CONFIG_DB_URL_KEY = 'session_pro_backend_db_url'
# Name of the endpoints exposed on the server
FLASK_ROUTE_ADD_PRO_PAYMENT = '/add_pro_payment'
FLASK_ROUTE_GENERATE_PRO_PROOF = '/generate_pro_proof'
FLASK_ROUTE_GET_PRO_REVOCATIONS = '/get_pro_revocations'
FLASK_ROUTE_GET_PRO_DETAILS = '/get_pro_details'
FLASK_ROUTE_SET_PAYMENT_REFUND_REQUESTED = '/set_payment_refund_requested'
# How many seconds can the timestamp in requests can differ from the Pro Backend's clock. This
# currently matches the storage server's store tolerance for onion-request forwarded messages as
# per:
#
# https://github.com/session-foundation/session-storage-server/blob/3d159a10d465678d758131c1075c9a6e5b4d95cc/oxenss/rpc/request_handler.h#L48
#
# We choose the upper-bound of tolerance for requests for maximum compatibility. Currently, in the
# flask context, no information is available to indicate if the request was forwarded or not so we
# default to assuming it is.
#
# All platforms are designed to interact with the backend using onion requests.
DEFAULT_TIMESTAMP_TOLERANCE_MS = 70 * 1000
SET_PAYMENT_REFUND_REQUESTED_HASH_PERSONALISATION = b'ProSetRefundReq_'
GET_PRO_PAYMENTS_DETAIL_HASH_PERSONALISATION = b'ProGetProDetReq_'
assert len(SET_PAYMENT_REFUND_REQUESTED_HASH_PERSONALISATION) == hashlib.blake2b.PERSON_SIZE
assert len(GET_PRO_PAYMENTS_DETAIL_HASH_PERSONALISATION) == hashlib.blake2b.PERSON_SIZE
# Generic response codes, note that we don't overlap custom status codes with these generic ones
# to try defensively avoid response handling code for callers.
RESPONSE_SUCCESS = 0
RESPONSE_GENERIC_ERROR = 1
RESPONSE_PARSE_ERROR = 2
class AddProPaymentStatus(enum.Enum):
Success = RESPONSE_SUCCESS
Error = RESPONSE_GENERIC_ERROR
AlreadyRedeemed = 100
UnknownPayment = 101
# The object containing routes that you register onto a Flask app to turn it
# into an app that accepts Session Pro Backend client requests.
flask_blueprint = flask.Blueprint('session-pro-backend-blueprint', __name__)
# All calls to time.time() in the server layer are now routed through the function pointer
# 'time_now'. This is primarily for unit tests which recorded real-time payment data on test
# networks that have had timestamps encoded in the past.
#
# Calls into the server layer were using the current system time (say for timestamp on signature
# validation) to prevent stale signatures that would break in those contexts. In the tests then
# the code changes the 'time_now()' implementation to "mock" the time of the server back to when the
# real-time data was being captured and tested.
time_now = lambda: time.time()
def make_error_response(status: int, errors: list[str]) -> flask.Response:
assert status != RESPONSE_SUCCESS, f"{RESPONSE_SUCCESS} is reserved for success"
result = flask.jsonify({'status': status, 'errors': errors})
return result
def make_success_response(dict_result: typing.Any) -> flask.Response:
result = flask.jsonify({'status': RESPONSE_SUCCESS, 'result': dict_result})
return result
def get_json_from_flask_request(request: flask.Request) -> GetJSONFromFlaskRequest:
# Get JSON from request
result: GetJSONFromFlaskRequest = GetJSONFromFlaskRequest()
try:
json_dict = typing.cast(dict[str, typing.Any] | None, json.loads(request.data))
if json_dict is None:
result.err_msg = "JSON failed to be parsed"
else:
result.json = json_dict
except Exception as e:
result.err_msg = f'JSON failed to be parsed: {e}'
return result
@contextlib.contextmanager
def get_db(flask_app: flask.Flask) -> collections.abc.Iterator[sqlalchemy.engine.Engine]:
database_url = typing.cast(str, flask_app.config[FLASK_CONFIG_DB_URL_KEY])
with db.open_database(database_url) as engine:
yield engine
def init(testing_mode: bool, database_url: str, server_x25519_skey: nacl.public.PrivateKey) -> flask.Flask:
result = flask.Flask(__name__)
result.config['TESTING'] = testing_mode
result.config[FLASK_CONFIG_DB_URL_KEY] = database_url
result.config[onion_req.FLASK_CONFIG_ONION_REQ_X25519_SKEY] = server_x25519_skey
result.register_blueprint(flask_blueprint)
result.register_blueprint(onion_req.flask_blueprint_v4)
return result
@flask_blueprint.route(FLASK_ROUTE_ADD_PRO_PAYMENT, methods=['POST'])
def add_pro_payment():
# Get JSON from request
get: GetJSONFromFlaskRequest = get_json_from_flask_request(flask.request)
if len(get.err_msg):
return make_error_response(status=RESPONSE_PARSE_ERROR, errors=[get.err_msg])
# Extract values from JSON
err = base.ErrorSink()
version: int = base.json_dict_require_int(d=get.json, key='version', err=err)
master_pkey: str = base.json_dict_require_str(d=get.json, key='master_pkey', err=err)
rotating_pkey: str = base.json_dict_require_str(d=get.json, key='rotating_pkey', err=err)
master_sig: str = base.json_dict_require_str(d=get.json, key='master_sig', err=err)
rotating_sig: str = base.json_dict_require_str(d=get.json, key='rotating_sig', err=err)
payment_tx: dict[str, base.JSONValue] = base.json_dict_require_obj(d=get.json, key='payment_tx', err=err)
payment_provider: int = base.json_dict_require_int(d=payment_tx, key='provider', err=err)
if len(err.msg_list):
return make_error_response(status=RESPONSE_PARSE_ERROR, errors=err.msg_list)
# Parse and validate values
if version != 0:
err.msg_list.append(f'Unrecognised version passed: {version}')
_ = base.verify_payment_provider(payment_provider=payment_provider, err=err)
# Build payment TX
if len(err.msg_list):
return make_error_response(status=RESPONSE_PARSE_ERROR, errors=err.msg_list)
user_payment = backend.UserPaymentTransaction()
user_payment.provider = base.PaymentProvider(payment_provider)
if user_payment.provider == base.PaymentProvider.GooglePlayStore:
user_payment.google_payment_token = base.json_dict_require_str(d=payment_tx, key='google_payment_token', err=err)
user_payment.google_order_id = base.json_dict_require_str(d=payment_tx, key='google_order_id', err=err)
elif user_payment.provider == base.PaymentProvider.iOSAppStore:
user_payment.apple_tx_id = base.json_dict_require_str(d=payment_tx, key='apple_tx_id', err=err)
elif user_payment.provider == base.PaymentProvider.Rangeproof:
# TODO: For now we do not support a user claiming a payment granted by Rangeproof via the
# server. These grants are written directly to the DB. Clients don't have a way of manually
# claiming a granted payment so this route is completely disabled.
# user_payment.rangeproof_order_id = base.json_dict_require_str(d=payment_tx, key='rangeproof_order_id', err=err)
err.msg_list.append(f'Bad payment provider given')
return make_error_response(status=RESPONSE_PARSE_ERROR, errors=err.msg_list)
else:
err.msg_list.append(f'Bad payment provider given')
return make_error_response(status=RESPONSE_PARSE_ERROR, errors=err.msg_list)
# Parse other components
master_pkey_bytes = base.hex_to_bytes(hex=master_pkey, label='Master public key', hex_len=nacl.bindings.crypto_sign_PUBLICKEYBYTES * 2, err=err)
rotating_pkey_bytes = base.hex_to_bytes(hex=rotating_pkey, label='Rotating public key', hex_len=nacl.bindings.crypto_sign_PUBLICKEYBYTES * 2, err=err)
master_sig_bytes = base.hex_to_bytes(hex=master_sig, label='Master key signature', hex_len=nacl.bindings.crypto_sign_BYTES * 2, err=err)
rotating_sig_bytes = base.hex_to_bytes(hex=rotating_sig, label='Rotating key signature', hex_len=nacl.bindings.crypto_sign_BYTES * 2, err=err)
if len(err.msg_list):
return make_error_response(status=RESPONSE_PARSE_ERROR, errors=err.msg_list)
# In dev mode, we allow some additional undocumented parameters to be added to the payload for
# development purposes
dev_add_pro_payment_args = backend.DevAddProPaymentArgs()
if base.DEV_BACKEND_MODE:
# NOTE: Sanity check dev mode
with get_db(flask.current_app) as engine:
backend.assert_backend_is_in_dev_mode(engine.connect())
plan_key = 'dev_plan'
duration_key = 'dev_duration_ms'
if plan_key in get.json:
plan_value = get.json[plan_key]
plan = base.ProPlan.from_string(typing.cast(str, plan_value))
if plan == None or dev_add_pro_payment_args.plan == base.ProPlan.Nil:
err.msg_list.append(f'{plan_key} must be specified as "OneMonth", "ThreeMonth" or "TwelveMonth", received: {plan_value}')
else:
dev_add_pro_payment_args.plan = plan
if duration_key in get.json:
dev_add_pro_payment_args.duration_ms = base.json_dict_require_int(d=get.json, key=duration_key, err=err)
if dev_add_pro_payment_args.duration_ms <= 0 or dev_add_pro_payment_args.duration_ms > (base.SECONDS_IN_YEAR * 1000):
err.msg_list.append(f'{duration_key} must be > 0 and <= year ind duration, received: {dev_add_pro_payment_args.duration_ms/1000}s')
dev_add_pro_payment_args.auto_renewing = base.json_dict_optional_bool(d=get.json,
key='dev_auto_renewing',
default=dev_add_pro_payment_args.auto_renewing,
err=err)
if len(err.msg_list):
return make_error_response(status=RESPONSE_PARSE_ERROR, errors=err.msg_list)
# Submit the payment to the DB
redeemed_payment = backend.RedeemPayment()
with get_db(flask.current_app) as engine:
with db.connection(engine) as conn:
runtime = backend.get_runtime(conn)
unix_ts_ms = int(time_now() * 1000)
redeemed_unix_ts_ms = backend.convert_unix_ts_ms_to_redeemed_unix_ts_ms(unix_ts_ms)
redeemed_payment = backend.verify_and_add_pro_payment(conn = conn,
version = version,
signing_key = runtime.backend_key,
unix_ts_ms = unix_ts_ms,
redeemed_unix_ts_ms = redeemed_unix_ts_ms,
master_pkey = nacl.signing.VerifyKey(master_pkey_bytes),
rotating_pkey = nacl.signing.VerifyKey(rotating_pkey_bytes),
payment_tx = user_payment,
master_sig = master_sig_bytes,
rotating_sig = rotating_sig_bytes,
err = err,
dev_args = dev_add_pro_payment_args)
if redeemed_payment.status != backend.RedeemPaymentStatus.Success:
status = AddProPaymentStatus.Error
if redeemed_payment.status == backend.RedeemPaymentStatus.AlreadyRedeemed:
status = AddProPaymentStatus.AlreadyRedeemed
elif redeemed_payment.status == backend.RedeemPaymentStatus.UnknownPayment:
status = AddProPaymentStatus.UnknownPayment
return make_error_response(status=status.value, errors=err.msg_list)
result = make_success_response(dict_result=redeemed_payment.proof.to_dict())
return result
@flask_blueprint.route(FLASK_ROUTE_GENERATE_PRO_PROOF, methods=['POST'])
def generate_pro_proof() -> flask.Response:
# Get JSON from request
get: GetJSONFromFlaskRequest = get_json_from_flask_request(flask.request)
if len(get.err_msg):
return make_error_response(status=RESPONSE_PARSE_ERROR, errors=[get.err_msg])
# Extract values from JSON
err = base.ErrorSink()
version: int = base.json_dict_require_int(d=get.json, key='version', err=err)
master_pkey: str = base.json_dict_require_str(d=get.json, key='master_pkey', err=err)
rotating_pkey: str = base.json_dict_require_str(d=get.json, key='rotating_pkey', err=err)
unix_ts_ms: int = base.json_dict_require_int(d=get.json, key='unix_ts_ms', err=err)
master_sig: str = base.json_dict_require_str(d=get.json, key='master_sig', err=err)
rotating_sig: str = base.json_dict_require_str(d=get.json, key='rotating_sig', err=err)
if len(err.msg_list):
return make_error_response(status=RESPONSE_PARSE_ERROR, errors=err.msg_list)
# Parse and validate values
if version != 0:
err.msg_list.append(f'Unrecognised version passed: {version}')
master_pkey_bytes = base.hex_to_bytes(hex=master_pkey, label='Master public key', hex_len=nacl.bindings.crypto_sign_PUBLICKEYBYTES * 2, err=err)
rotating_pkey_bytes = base.hex_to_bytes(hex=rotating_pkey, label='Rotating public key', hex_len=nacl.bindings.crypto_sign_PUBLICKEYBYTES * 2, err=err)
master_sig_bytes = base.hex_to_bytes(hex=master_sig, label='Master key signature', hex_len=nacl.bindings.crypto_sign_BYTES * 2, err=err)
rotating_sig_bytes = base.hex_to_bytes(hex=rotating_sig, label='Rotating key signature', hex_len=nacl.bindings.crypto_sign_BYTES * 2, err=err)
# Validate the timestamp is within 5 minutes of the current time (mitigate replay attacks)
now: int = int(time_now() * 1000)
max_unix_ts_ms: int = now + DEFAULT_TIMESTAMP_TOLERANCE_MS
min_unix_ts_ms: int = now - DEFAULT_TIMESTAMP_TOLERANCE_MS
if unix_ts_ms < min_unix_ts_ms:
err.msg_list.append(f'Nonce timestamp is too far in the past: {unix_ts_ms} (min {min_unix_ts_ms})')
if unix_ts_ms > max_unix_ts_ms:
err.msg_list.append(f'Nonce timestamp is too far in the future: {unix_ts_ms} (max {max_unix_ts_ms})')
if len(err.msg_list):
return make_error_response(status=RESPONSE_PARSE_ERROR, errors=err.msg_list)
# Request proof from the backend
with get_db(flask.current_app) as engine:
with db.connection(engine) as conn:
runtime = backend.get_runtime(conn)
proof = backend.generate_pro_proof(conn = conn,
version = version,
signing_key = runtime.backend_key,
gen_index_salt = runtime.gen_index_salt,
master_pkey = nacl.signing.VerifyKey(master_pkey_bytes),
rotating_pkey = nacl.signing.VerifyKey(rotating_pkey_bytes),
unix_ts_ms = unix_ts_ms,
master_sig = master_sig_bytes,
rotating_sig = rotating_sig_bytes,
err = err)
if len(err.msg_list):
return make_error_response(status=RESPONSE_GENERIC_ERROR, errors=err.msg_list)
result = make_success_response(dict_result=proof.to_dict())
return result
@flask_blueprint.route(FLASK_ROUTE_GET_PRO_REVOCATIONS, methods=['POST'])
def get_pro_revocations():
# Get JSON from request
get: GetJSONFromFlaskRequest = get_json_from_flask_request(flask.request)
if len(get.err_msg):
return make_error_response(status=RESPONSE_PARSE_ERROR, errors=[get.err_msg])
# Extract values from JSON
err = base.ErrorSink()
version: int = base.json_dict_require_int(d=get.json, key='version', err=err)
ticket: int = base.json_dict_require_int(d=get.json, key='ticket', err=err)
if len(err.msg_list):
return make_error_response(status=RESPONSE_PARSE_ERROR, errors=err.msg_list)
# Parse and validate values
if version != 0:
err.msg_list.append(f'Unrecognised version passed: {version}')
if len(err.msg_list):
return make_error_response(status=RESPONSE_PARSE_ERROR, errors=err.msg_list)
RETRY_IN_S = base.SECONDS_IN_DAY
revocation_items: list[dict[str, str | int]] = []
revocation_ticket: int = 0
with get_db(flask.current_app) as engine:
with db.connection(engine) as conn:
with db.transaction(conn) as tx:
runtime_row = db.query_one(tx.conn, "SELECT revocation_ticket FROM runtime")
revocation_ticket = runtime_row[0] if runtime_row else 0
if ticket < revocation_ticket:
runtime = backend.get_runtime_tx(tx)
for row in backend.get_pro_revocations_iterator_tx(tx):
gen_index: int = row[0]
creation_unix_ts_ms: int = row[1]
expiry_unix_ts_ms: int = row[2]
gen_index_hash: bytes = backend.make_gen_index_hash(gen_index=gen_index, gen_index_salt=runtime.gen_index_salt)
assert gen_index < runtime.gen_index, f"lhs={gen_index}, rhs={runtime.gen_index}"
assert len(runtime.gen_index_salt) == hashlib.blake2b.SALT_SIZE
expiry_unix_ts_ms = base.round_unix_ts_ms_to_next_day(expiry_unix_ts_ms)
effective_unix_ts_ms: int = min(creation_unix_ts_ms + (RETRY_IN_S * 1000), expiry_unix_ts_ms)
revocation_items.append({
'expiry_unix_ts_ms': expiry_unix_ts_ms,
'gen_index_hash': gen_index_hash.hex(),
'effective_unix_ts_ms': effective_unix_ts_ms,
})
if len(err.msg_list):
return make_error_response(status=RESPONSE_GENERIC_ERROR, errors=err.msg_list)
result = make_success_response(dict_result={
'version': version,
'ticket': revocation_ticket,
'items': revocation_items,
'retry_in_s': RETRY_IN_S,
})
return result
@flask_blueprint.route(FLASK_ROUTE_GET_PRO_DETAILS, methods=['POST'])
def get_pro_details():
# Get JSON from request
get: GetJSONFromFlaskRequest = get_json_from_flask_request(flask.request)
if len(get.err_msg):
return make_error_response(status=RESPONSE_PARSE_ERROR, errors=[get.err_msg])
# Extract values from JSON
err = base.ErrorSink()
version: int = base.json_dict_require_int(d=get.json, key='version', err=err)
master_pkey: str = base.json_dict_require_str(d=get.json, key='master_pkey', err=err)
master_sig: str = base.json_dict_require_str(d=get.json, key='master_sig', err=err)
unix_ts_ms: int = base.json_dict_require_int(d=get.json, key='unix_ts_ms', err=err)
count: int = base.json_dict_require_int(d=get.json, key='count', err=err)
if len(err.msg_list):
return make_error_response(status=RESPONSE_PARSE_ERROR, errors=err.msg_list)
# Parse and validate values
if version != 0:
err.msg_list.append(f'Unrecognised version passed: {version}')
master_pkey_bytes = base.hex_to_bytes(hex=master_pkey, label='Master public key', hex_len=nacl.bindings.crypto_sign_PUBLICKEYBYTES * 2, err=err)
master_sig_bytes = base.hex_to_bytes(hex=master_sig, label='Master key signature', hex_len=nacl.bindings.crypto_sign_BYTES * 2, err=err)
if count < 0:
err.msg_list.append(f'Count was negative: {count}')
# Validate timestamp
# TODO: We _could_ track the last GET_ALL_PAYMENTS_MAX_TIMESTAMP_DELTA_S seconds worth of
# requests to completely reject replay attacks if we cared enough but onion requests probably
# suffice to mask the ability to replay a query.
timestamp_delta: float = (time_now() * 1000) - float(unix_ts_ms)
if abs(timestamp_delta) >= DEFAULT_TIMESTAMP_TOLERANCE_MS:
err.msg_list.append(f'Timestamp is too old to permit retrieval of payments, delta was {timestamp_delta}ms')
if len(err.msg_list):
return make_error_response(status=RESPONSE_PARSE_ERROR, errors=err.msg_list)
# Validate the signature
master_pkey_nacl = nacl.signing.VerifyKey(master_pkey_bytes)
hash_to_verify: bytes = backend.make_get_pro_details_hash(version=version, master_pkey=master_pkey_nacl, unix_ts_ms=unix_ts_ms, count=count)
try:
_ = master_pkey_nacl.verify(smessage=hash_to_verify, signature=master_sig_bytes)
except Exception as e:
err.msg_list.append('Signature failed to be verified')
return make_error_response(status=RESPONSE_PARSE_ERROR, errors=err.msg_list)
items: list[dict[str, str | int | bool]] = []
user_pro_status: UserProStatus = UserProStatus.NeverBeenPro
auto_renewing = False
expiry_unix_ts_ms = 0
grace_period_duration_ms = 0
payments_total = 0
refund_requested_unix_ts_ms = 0
# NOTE: Eventually we might migrate this to be a fully-featured enum to provide some more
# descriptive messaging
error_report: int = False
with get_db(flask.current_app) as engine:
with db.connection(engine) as conn:
with db.transaction(conn) as tx:
error_report = int(backend.has_user_error_from_master_pkey_tx(tx, master_pkey_nacl))
get_user: backend.GetUserAndPayments = backend.get_user_and_payments(tx=tx, master_pkey=master_pkey_nacl)
grace_period_duration_ms = get_user.user.grace_period_duration_ms
expiry_unix_ts_ms = get_user.user.expiry_unix_ts_ms
auto_renewing = get_user.user.auto_renewing
payments_total = get_user.payments_count
refund_requested_unix_ts_ms = get_user.user.refund_requested_unix_ts_ms
# NOTE: Collect payment history
if count > 0:
for row in get_user.payments_it:
if len(items) >= count:
break
row_tuple: backend.SQLTablePaymentRowTuple = tuple(row)
payment: backend.PaymentRow = backend.payment_row_from_tuple(row_tuple)
# NOTE: We do not return unredeemed payments. This payment token/tx IDs are
# confidential until the user actually registers the token themselves which they
# should witness from the payment provider independently from us so there should be
# no need to reveal this to the user until they've confirmed their own receipt of
# it.