taler-mailbox

Service for asynchronous wallet-to-wallet payment messages
Log | Files | Refs | Submodules | README | LICENSE

mailbox.go (29550B)


      1 // This file is part of taler-mailbox, the Taler Mailbox implementation.
      2 // Copyright (C) 2022 Martin Schanzenbach
      3 //
      4 // Taler-mailbox is free software: you can redistribute it and/or modify it
      5 // under the terms of the GNU Affero General Public License as published
      6 // by the Free Software Foundation, either version 3 of the License,
      7 // or (at your option) any later version.
      8 //
      9 // Taler-mailbox is distributed in the hope that it will be useful, but
     10 // WITHOUT ANY WARRANTY; without even the implied warranty of
     11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
     12 // Affero General Public License for more details.
     13 //
     14 // You should have received a copy of the GNU Affero General Public License
     15 // along with this program.  If not, see <http://www.gnu.org/licenses/>.
     16 //
     17 // SPDX-License-Identifier: AGPL3.0-or-later
     18 
     19 // Package mailbox is a GNU Taler service. See https://docs.taler.net/core/api-mailbox.html
     20 package mailbox
     21 
     22 import (
     23 	"crypto/ed25519"
     24 	"crypto/sha512"
     25 	"encoding/binary"
     26 	"encoding/json"
     27 	"fmt"
     28 	"io"
     29 	"log"
     30 	"net/http"
     31 	"os"
     32 	"strconv"
     33 	"strings"
     34 	"time"
     35 
     36 	"github.com/gorilla/mux"
     37 	"github.com/schanzen/taler-go/pkg/merchant"
     38 	tos "github.com/schanzen/taler-go/pkg/rest"
     39 	talerutil "github.com/schanzen/taler-go/pkg/util"
     40 	"taler.net/taler-mailbox/internal/gana"
     41 	"taler.net/taler-mailbox/internal/util"
     42 )
     43 
     44 type LogLevel int
     45 
     46 const (
     47 	LogError LogLevel = iota
     48 	LogWarning
     49 	LogInfo
     50 	LogDebug
     51 )
     52 
     53 var LoglevelStringMap = map[LogLevel]string{
     54 	LogDebug:   "DEBUG",
     55 	LogError:   "ERROR",
     56 	LogWarning: "WARN",
     57 	LogInfo:    "INFO",
     58 }
     59 
     60 // ErrorDetail is the detailed error payload returned from Mailbox endpoints
     61 type ErrorDetail struct {
     62 
     63 	// Numeric error code unique to the condition.
     64 	// The other arguments are specific to the error value reported here.
     65 	Code int `json:"code"`
     66 
     67 	// Human-readable description of the error, i.e. "missing parameter", "commitment violation", ...
     68 	// Should give a human-readable hint about the error's nature. Optional, may change without notice!
     69 	Hint string `json:"hint,omitempty"`
     70 
     71 	// Optional detail about the specific input value that failed. May change without notice!
     72 	Detail string `json:"detail,omitempty"`
     73 }
     74 
     75 
     76 type MailboxConfig struct {
     77 	// libtool-style representation of the Mailbox protocol version, see
     78 	// https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
     79 	// The format is "current:revision:age".
     80 	LibtoolVersion string
     81 
     82 	// Version
     83 	Version string
     84 
     85 	// Data home
     86 	Datahome string
     87 
     88 	// Configuration
     89 	Ini talerutil.TalerConfiguration
     90 
     91 	// The database connection to use
     92 	DB *MailboxDatabase
     93 
     94 	// Merchant connection
     95 	Merchant merchant.Merchant
     96 
     97 	// Loglevel
     98 	Loglevel LogLevel
     99 }
    100 
    101 // Mailbox is the primary object of the Mailbox service
    102 type Mailbox struct {
    103 
    104 	// The main router
    105 	Router *mux.Router
    106 
    107 	// The database connection to use
    108 	DB *MailboxDatabase
    109 
    110 	// Our configuration from the ini
    111 	Cfg MailboxConfig
    112 
    113 	// Fixed size of message bodies
    114 	MessageBodyBytes int64 `json:"message_body_bytes"`
    115 
    116 	// Merchant object
    117 	Merchant merchant.Merchant
    118 
    119 	// Base URL
    120 	BaseURL string
    121 
    122 	// Registration fee for each validity month mailbox
    123 	MonthlyFee *talerutil.Amount
    124 
    125 	// Registration fee for registering or modifying mailbox
    126 	RegistrationUpdateFee *talerutil.Amount
    127 
    128 	// Message fee for receiving a message
    129 	MessageFee *talerutil.Amount
    130 
    131 	// The free message quota
    132 	FreeMessageQuota uint64
    133 
    134 	// How many messages will a single response
    135 	// contain at maximum.
    136 	MessageResponseLimit uint64
    137 
    138 	// Logger
    139 	Logger *log.Logger
    140 
    141 	// Currency Spec
    142 	CurrencySpec talerutil.CurrencySpecification
    143 }
    144 
    145 type RelativeTime struct {
    146 	Microseconds uint64 `json:"d_us"`
    147 }
    148 
    149 // 1 Month as Go duration
    150 const monthDuration = time.Hour * 24 * 30
    151 
    152 // VersionResponse is the JSON response of the /config endpoint
    153 type VersionResponse struct {
    154 	// libtool-style representation of the Mailbox protocol version, see
    155 	// https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning
    156 	// The format is "current:revision:age".
    157 	Version string `json:"version"`
    158 
    159 	// Name of the protocol.
    160 	Name string `json:"name"` // "taler-mailbox"
    161 
    162 	// Fixed size of message bodies
    163 	MessageBodyBytes int64 `json:"message_body_bytes"`
    164 
    165 	// How long will the service store a message
    166 	// before giving up
    167 	DeliveryPeriod RelativeTime `json:"delivery_period" gorm:"embedded;embeddedPrefix:delivery_period_"`
    168 
    169 	// How many messages will a single response
    170 	// contain at maximum.
    171 	MessageResponseLimit uint64 `json:"message_response_limit"`
    172 
    173 	// How much is the cost of a single
    174 	// registration (update) of a mailbox
    175 	// May be 0 for a free update/registration.
    176 	RegistrationUpdateFee string `json:"registration_update_fee"`
    177 
    178 	// How much is the cost of a single
    179 	// registration period (30 days) of a mailbox
    180 	// May be 0 for a free registration.
    181 	MonthlyFee string `json:"monthly_fee"`
    182 
    183 	// How much is the cost to send a single
    184 	// message to a mailbox.
    185 	// May be 0 for free message sending.
    186 	MessageFee string `json:"message_fee"`
    187 
    188 	// How many messages can be send and
    189 	// are stored by the service for free.
    190 	// After the quota is reached, the
    191 	// regular message_fee applies.
    192 	// May be 0 for no free quota.
    193 	FreeMessageQuota uint64 `json:"free_message_quota"`
    194 }
    195 
    196 type MailboxRegistrationRequest struct {
    197 
    198 	// Keys to add/update for a mailbox.
    199 	MailboxMetadata MailboxMetadata `json:"mailbox_metadata"`
    200 
    201 	// Signature by the mailbox's signing key affirming
    202 	// the update of keys, of purpose
    203 	// TALER_SIGNATURE_WALLET_MAILBOX_KEYS_UPDATE.
    204 	// The signature is created over the SHA-512 hash
    205 	// of (encryptionKeyType||encryptionKey||expiration)
    206 	Signature string `json:"signature"`
    207 }
    208 
    209 // MailboxRateLimitedResponse is the JSON response when a rate limit is hit
    210 type MailboxRateLimitedResponse struct {
    211 
    212 	// Taler error code, TALER_EC_mailbox_REGISTER_RATE_LIMITED.
    213 	Code int `json:"code"`
    214 
    215 	// When the client should retry. Currently: In microseconds
    216 	RetryDelay int64 `json:"retry_delay"`
    217 
    218 	// The human readable error message.
    219 	Hint string `json:"hint"`
    220 }
    221 
    222 // configResponse returns the service configuration.
    223 //
    224 // @Summary     Get service configuration
    225 // @Description Returns service metadata including fees, message size limits, and delivery period.
    226 // @Tags        config
    227 // @Produce     json
    228 // @Success     200 {object} VersionResponse
    229 // @Router      /config [get]
    230 func (m *Mailbox) configResponse(w http.ResponseWriter, r *http.Request) {
    231 	dp, err := m.Cfg.Ini.GetDuration("mailbox", "delivery_period", 3*24*time.Hour)
    232 	if err != nil {
    233 		log.Fatal(err)
    234 	}
    235 	cfg := VersionResponse{
    236 		Version:               m.Cfg.LibtoolVersion,
    237 		Name:                  "taler-mailbox",
    238 		MessageBodyBytes:      m.MessageBodyBytes,
    239 		MessageResponseLimit:  m.MessageResponseLimit,
    240 		MonthlyFee:            m.MonthlyFee.String(),
    241 		RegistrationUpdateFee: m.RegistrationUpdateFee.String(),
    242 		DeliveryPeriod:        RelativeTime{Microseconds: uint64(dp.Microseconds())},
    243 		MessageFee:            m.MessageFee.String(),
    244 		FreeMessageQuota:      m.FreeMessageQuota,
    245 	}
    246 	w.Header().Set("Content-Type", "application/json")
    247 	response, _ := json.Marshal(cfg)
    248 	w.Write(response)
    249 }
    250 
    251 // getMessagesResponse retrieves pending messages for a mailbox.
    252 //
    253 // @Summary     Retrieve messages
    254 // @Description Returns up to MessageResponseLimit encrypted message bodies for the given mailbox.
    255 // @Description The ETag response header contains the serial number of the first message.
    256 // @Tags        mailbox
    257 // @Produce     application/octet-stream
    258 // @Param       h_mailbox path   string true "SHA-512 hash of the mailbox signing key (Crockford base32)"
    259 // @Success     200       "One or more message bodies concatenated"
    260 // @Success     204       "No messages available"
    261 // @Failure     404       "Mailbox not found"
    262 // @Router      /{h_mailbox} [get]
    263 func (m *Mailbox) getMessagesResponse(w http.ResponseWriter, r *http.Request) {
    264 	vars := mux.Vars(r)
    265 	//to, toSet := vars["timeout_ms"]
    266 	var entries []InboxEntry
    267 	// FIXME rate limit
    268 	// FIXME timeout
    269 	// FIXME possibly limit results here
    270 	m.checkPendingMailboxRegistrationUpdates(vars["h_mailbox"])
    271 	entries, err := m.DB.GetMessages(vars["h_mailbox"], int(m.MessageResponseLimit))
    272 	if err != nil {
    273 		m.Logf(LogError, "Error getting messages: %v", err)
    274 		w.WriteHeader(http.StatusNotFound)
    275 		return
    276 	}
    277 	if len(entries) == 0 {
    278 		w.WriteHeader(http.StatusNoContent)
    279 		return
    280 	}
    281 	// Add ETag of first message ID
    282 	etag := entries[0].Serial
    283 	w.Header().Add("ETag", fmt.Sprintf("%d", etag))
    284 	for _, entry := range entries {
    285 		w.Write(entry.Body)
    286 	}
    287 }
    288 
    289 // sendMessageResponse delivers a message to a mailbox.
    290 //
    291 // @Summary     Send a message
    292 // @Description Stores an encrypted message body for the given mailbox. The body must be
    293 // @Description exactly MessageBodyBytes in size.
    294 // @Tags        mailbox
    295 // @Accept      application/octet-stream
    296 // @Param       h_mailbox path   string true "SHA-512 hash of the mailbox signing key (Crockford base32)"
    297 // @Param       body       body   string true "Encrypted message body (fixed size)"
    298 // @Success     204        "Message stored"
    299 // @Success     304        "Identical message already stored"
    300 // @Failure     400        "Missing or wrong-size body"
    301 // @Failure     402        "Payment required (free quota exceeded)"
    302 // @Failure     500
    303 // @Router      /{h_mailbox} [post]
    304 func (m *Mailbox) sendMessageResponse(w http.ResponseWriter, r *http.Request) {
    305 	vars := mux.Vars(r)
    306 	var entry InboxEntry
    307 	var errDetail ErrorDetail
    308 	if r.Body == nil {
    309 		errDetail.Code = gana.WALLET_MAILBOX_MESSAGE_SEND_DENIED
    310 		errDetail.Hint = "No request body."
    311 		resp, _ := json.Marshal(errDetail)
    312 		w.WriteHeader(http.StatusForbidden)
    313 		w.Write(resp)
    314 		return
    315 	}
    316 	if r.ContentLength != m.MessageBodyBytes {
    317 		errDetail.Code = gana.WALLET_MAILBOX_MESSAGE_SEND_DENIED
    318 		errDetail.Hint = "Wrong message size."
    319 		resp, _ := json.Marshal(errDetail)
    320 		w.WriteHeader(http.StatusForbidden)
    321 		w.Write(resp)
    322 		return
    323 	}
    324 	body, err := io.ReadAll(r.Body)
    325 	if err != nil {
    326 		log.Printf("%v", err)
    327 		errDetail.Code = gana.WALLET_MAILBOX_MESSAGE_SEND_DENIED
    328 		errDetail.Hint = "Cannot read body."
    329 		resp, _ := json.Marshal(errDetail)
    330 		w.WriteHeader(http.StatusForbidden)
    331 		w.Write(resp)
    332 		return
    333 	}
    334 	if !m.MessageFee.IsZero() {
    335 		var count int64
    336 		count, err = m.DB.GetMessagesCount(vars["h_mailbox"])
    337 		if nil != err {
    338 			m.Logf(LogError, "Error getting messages: %v", err)
    339 			http.Error(w, "Cannot look for entries", http.StatusBadRequest)
    340 			return
    341 		}
    342 		if count >= int64(m.FreeMessageQuota) {
    343 			w.WriteHeader(http.StatusPaymentRequired)
    344 			//w.Header().Set("Taler", payto) FIXME generate payto
    345 			return
    346 		}
    347 	}
    348 	m.checkPendingMailboxRegistrationUpdates(vars["h_mailbox"])
    349 	err = m.DB.GetInboxEntryBySigningKeyAndBody(&entry, vars["h_mailbox"], body)
    350 	if err == nil {
    351 		w.WriteHeader(http.StatusNotModified)
    352 		return
    353 	}
    354 	entry.HashedSigningKey = vars["h_mailbox"]
    355 	entry.Body = body
    356 	err = m.DB.InsertInboxEntry(&entry)
    357 	if err != nil {
    358 		m.Logf(LogError, "Error storing message: %v", err)
    359 		w.WriteHeader(http.StatusInternalServerError)
    360 		return
    361 	}
    362 	w.WriteHeader(http.StatusNoContent)
    363 }
    364 
    365 // getKeysResponse returns the public key metadata for a registered mailbox.
    366 //
    367 // @Summary     Get mailbox info
    368 // @Description Returns the signing and encryption key metadata for the given mailbox.
    369 // @Tags        mailbox
    370 // @Produce     json
    371 // @Param       h_mailbox path   string true "SHA-512 hash of the mailbox signing key (Crockford base32)"
    372 // @Success     200        {object} MailboxMetadata
    373 // @Failure     404        "Mailbox not found or expired"
    374 // @Router      /info/{h_mailbox} [get]
    375 func (m *Mailbox) getKeysResponse(w http.ResponseWriter, r *http.Request) {
    376 	vars := mux.Vars(r)
    377 	var keyEntry MailboxMetadata
    378 	m.checkPendingMailboxRegistrationUpdates(vars["h_mailbox"])
    379 	err := m.DB.GetMailboxMetadataBySigningKey(&keyEntry, vars["h_mailbox"])
    380 	if err != nil {
    381 		m.Logf(LogError, "Error finding mailbox: %v", err)
    382 		w.WriteHeader(http.StatusNotFound)
    383 		return
    384 	}
    385 	m.Logf(LogDebug, "entry expires at %d, have %d", keyEntry.Expiration, time.Now().Unix())
    386 	if int64(keyEntry.Expiration.Seconds) < int64(time.Now().Unix()) {
    387 		w.WriteHeader(http.StatusNotFound)
    388 		return
    389 	}
    390 	response, _ := json.Marshal(keyEntry)
    391 	w.Write(response)
    392 }
    393 
    394 func (m *Mailbox) validateRegistrationSignature(msg MailboxRegistrationRequest) error {
    395 	var expNbo [8]byte
    396 	var signedMsg [72]byte
    397 	encPk, err := util.Base32CrockfordDecode(msg.MailboxMetadata.EncryptionKey, 32)
    398 	if err != nil {
    399 		return fmt.Errorf("unable to decode encryption key")
    400 	}
    401 	pkey, err := util.Base32CrockfordDecode(msg.MailboxMetadata.SigningKey, 32)
    402 	if err != nil {
    403 		return fmt.Errorf("unable to decode pubkey")
    404 	}
    405 	pk := ed25519.PublicKey(pkey)
    406 	sig, err := util.Base32CrockfordDecode(msg.Signature, 64)
    407 	if nil != err {
    408 		return fmt.Errorf("unable to decode signature")
    409 	}
    410 	binary.BigEndian.PutUint64(expNbo[:], msg.MailboxMetadata.Expiration.Seconds)
    411 	size := signedMsg[0:4]
    412 	binary.BigEndian.PutUint32(size, 64+4+4)
    413 	purp := signedMsg[4:8]
    414 	binary.BigEndian.PutUint32(purp, gana.TalerSignaturePurposeMailboxRegister)
    415 	h := sha512.New()
    416 	h.Write([]byte(msg.MailboxMetadata.EncryptionKeyType)) // Currently always X25519
    417 	h.Write(encPk)
    418 	h.Write(expNbo[:])
    419 	copy(signedMsg[8:], h.Sum(nil))
    420 	if !ed25519.Verify(pk, signedMsg[0:], sig) {
    421 		return fmt.Errorf("signature invalid")
    422 	}
    423 	return nil
    424 }
    425 
    426 // Check if this is a non-zero, positive amount
    427 func calculateCost(sliceCostAmount string, fixedCostAmount string, howLong time.Duration, sliceDuration time.Duration) (*talerutil.Amount, error) {
    428 	sliceCount := int(float64(howLong.Microseconds()) / float64(sliceDuration.Microseconds()))
    429 	sliceCost, err := talerutil.ParseAmount(sliceCostAmount)
    430 	if nil != err {
    431 		return nil, err
    432 	}
    433 	fixedCost, err := talerutil.ParseAmount(fixedCostAmount)
    434 	if nil != err {
    435 		return nil, err
    436 	}
    437 	sum := &talerutil.Amount{
    438 		Currency: sliceCost.Currency,
    439 		Value:    0,
    440 		Fraction: 0,
    441 	}
    442 	for range sliceCount {
    443 		sum, err = sum.Add(*sliceCost)
    444 		if nil != err {
    445 			return nil, err
    446 		}
    447 	}
    448 	sum, err = sum.Add(*fixedCost)
    449 	if nil != err {
    450 		return nil, err
    451 	}
    452 	return sum, nil
    453 }
    454 
    455 // registerMailboxResponse registers or updates a mailbox.
    456 //
    457 // @Summary     Register or update mailbox
    458 // @Description Registers a new mailbox or updates the keys/expiration of an existing one.
    459 // @Description A valid EdDSA signature over the key material must be provided.
    460 // @Tags        mailbox
    461 // @Accept      json
    462 // @Param       body body MailboxRegistrationRequest true "Registration request"
    463 // @Success     204  "Registration confirmed"
    464 // @Success     304  "Nothing changed"
    465 // @Failure     400  "Invalid request body or signature"
    466 // @Failure     402  "Payment required"
    467 // @Failure     500
    468 // @Router      /register [post]
    469 func (m *Mailbox) registerMailboxResponse(w http.ResponseWriter, r *http.Request) {
    470 	var msg MailboxRegistrationRequest
    471 	var pendingRegistration PendingMailboxRegistration
    472 	var registrationEntry MailboxMetadata
    473 	if r.Body == nil {
    474 		m.Logf(LogError, "no request body")
    475 		http.Error(w, "No request body", http.StatusBadRequest)
    476 		return
    477 	}
    478 	err := json.NewDecoder(r.Body).Decode(&msg)
    479 	if err != nil {
    480 		http.Error(w, "Malformed request body", http.StatusBadRequest)
    481 		return
    482 	}
    483 	pkey, err := util.Base32CrockfordDecode(msg.MailboxMetadata.SigningKey, 32)
    484 	if err != nil {
    485 		http.Error(w, "Public key invalid", http.StatusBadRequest)
    486 		return
    487 	}
    488 	pk := ed25519.PublicKey(pkey)
    489 	err = m.validateRegistrationSignature(msg)
    490 	var errDetail ErrorDetail
    491 	if nil != err {
    492 		errDetail.Code = gana.WALLET_MAILBOX_REGISTER_INVALID_SIGNATURE
    493 		errDetail.Hint = "Signature invalid."
    494 		resp, _ := json.Marshal(errDetail)
    495 		w.WriteHeader(http.StatusForbidden)
    496 		w.Write(resp)
    497 		return
    498 	}
    499 	h := sha512.New()
    500 	h.Write(pkey)
    501 	hMailbox := util.Base32CrockfordEncode(h.Sum(nil))
    502 	pendingRegistration.HashedSigningKey = hMailbox
    503 	// Round to the nearest multiple of a month
    504 	reqExpiration := time.Unix(int64(msg.MailboxMetadata.Expiration.Seconds), 0)
    505 	now := time.Now()
    506 	reqDuration := reqExpiration.Sub(now).Round(monthDuration)
    507 	err = m.DB.GetMailboxMetadataBySigningKey(&registrationEntry, hMailbox)
    508 	if err == nil {
    509 		// This probably means the registration is modified or extended or both
    510 		entryModified := (registrationEntry.EncryptionKey != msg.MailboxMetadata.EncryptionKey)
    511 		// At least one MonthlyFee
    512 		if reqDuration.Microseconds() == 0 && !entryModified {
    513 			// Nothing changed. Return validity
    514 			w.WriteHeader(http.StatusNotModified)
    515 			return
    516 		}
    517 	} else {
    518 		// Entry does not yet exist, add but immediately expire it
    519 		registrationEntry = msg.MailboxMetadata
    520 		registrationEntry.Expiration.Seconds = uint64(time.Now().Unix() - 1)
    521 		hAddr := sha512.New()
    522 		hAddr.Write(pk)
    523 		registrationEntry.HashedSigningKey = util.Base32CrockfordEncode(hAddr.Sum(nil))
    524 		err = m.DB.InsertMailboxRegistration(&registrationEntry)
    525 		if nil != err {
    526 			m.Logf(LogError, "%v\n", err)
    527 			w.WriteHeader(http.StatusInternalServerError)
    528 			return
    529 		}
    530 	}
    531 	err = m.DB.GetPendingMailboxRegistrationBySigningKey(&pendingRegistration, hMailbox)
    532 	pendingRegistrationExists := (nil == err)
    533 	if !pendingRegistrationExists {
    534 		pendingRegistration.HashedSigningKey = hMailbox
    535 		pendingRegistration.Duration = reqDuration.Microseconds()
    536 		err = m.DB.InsertPendingMailboxRegistration(&pendingRegistration)
    537 		if nil != err {
    538 			m.Logf(LogError, "Error inserting pending registration: %v\n", err)
    539 			w.WriteHeader(http.StatusInternalServerError)
    540 			return
    541 		}
    542 		err = m.DB.GetPendingMailboxRegistrationBySigningKey(&pendingRegistration, hMailbox)
    543 		if nil != err {
    544 			m.Logf(LogError, "Error getting pending registration: %v\n", err)
    545 			w.WriteHeader(http.StatusInternalServerError)
    546 			return
    547 		}
    548 	}
    549 	// At least the update fee needs to be paid
    550 	cost, err := calculateCost(m.MonthlyFee.String(),
    551 		m.RegistrationUpdateFee.String(),
    552 		reqDuration,
    553 		monthDuration)
    554 	if err != nil {
    555 		m.Logf(LogError, "Error calculating cost: %v\n", err)
    556 		w.WriteHeader(http.StatusInternalServerError)
    557 		return
    558 	}
    559 	if !cost.IsZero() {
    560 		if len(pendingRegistration.OrderID) == 0 {
    561 			// Add new order
    562 			orderID, newOrderErr := m.Merchant.AddNewOrder(*cost, "Mailbox registration", m.BaseURL)
    563 			if newOrderErr != nil {
    564 				m.Logf(LogError, "Error adding order: %v", newOrderErr)
    565 				w.WriteHeader(http.StatusInternalServerError)
    566 				return
    567 			}
    568 			m.Logf(LogDebug, "New order ID %s for pending registration for %s", orderID, pendingRegistration.HashedSigningKey)
    569 			pendingRegistration.OrderID = orderID
    570 		}
    571 		// Check if order paid.
    572 		// FIXME: Remember that it was activated and paid
    573 		// FIXME: We probably need to handle the return code here (see gns registrar for how)
    574 		_, _, payto, paytoErr := m.Merchant.IsOrderPaid(pendingRegistration.OrderID)
    575 		if paytoErr != nil {
    576 			fmt.Println(paytoErr)
    577 			w.WriteHeader(http.StatusInternalServerError)
    578 			m.Logf(LogError, "Error checking if order is paid: %s\n", paytoErr.Error())
    579 			return
    580 		}
    581 		if len(payto) != 0 {
    582 			err = m.DB.UpdatePendingMailboxRegistrationOrderId(&pendingRegistration)
    583 			if err != nil {
    584 				m.Logf(LogError, "Error updating pending registration: %v\n", err)
    585 				w.WriteHeader(http.StatusInternalServerError)
    586 				return
    587 			}
    588 			w.WriteHeader(http.StatusPaymentRequired)
    589 			w.Header().Set("Taler", payto)
    590 			return
    591 		}
    592 	}
    593 	// Update expiration time of registration.
    594 	registrationEntry.Expiration.Seconds += uint64(reqDuration.Seconds())
    595 	_, err = m.DB.DeletePendingRegistration(&pendingRegistration)
    596 	if nil != err {
    597 		m.Logf(LogError, "Error deleting pending registration: %v\n", err)
    598 		w.WriteHeader(http.StatusInternalServerError)
    599 		return
    600 	}
    601 	err = m.DB.UpdateMailboxExpiration(&registrationEntry)
    602 	if nil != err {
    603 		m.Logf(LogError, "Error updating mailbox registration: %v\n", err)
    604 		w.WriteHeader(http.StatusInternalServerError)
    605 		return
    606 	}
    607 	w.WriteHeader(http.StatusNoContent)
    608 }
    609 
    610 func (m *Mailbox) checkPendingMailboxRegistrationUpdates(hMailbox string) {
    611 	var pendingEntry PendingMailboxRegistration
    612 	var registrationEntry MailboxMetadata
    613 	err := m.DB.GetPendingMailboxRegistrationBySigningKey(&pendingEntry, hMailbox)
    614 	if err != nil {
    615 		return
    616 	}
    617 	m.Logf(LogDebug, "Found pending registration for %s, OrderID: %s", hMailbox, pendingEntry.OrderID)
    618 	rc, orderStatus, _, paytoErr := m.Merchant.IsOrderPaid(pendingEntry.OrderID)
    619 	if nil != paytoErr {
    620 		if rc == http.StatusNotFound {
    621 			m.Logf(LogInfo, "Registration order for `%s' not found, removing\n", hMailbox)
    622 		}
    623 		_, err = m.DB.DeletePendingRegistration(&pendingEntry)
    624 		if nil != err {
    625 			m.Logf(LogInfo, "Error deleting pending registration: %v\n", err)
    626 		}
    627 		return
    628 	}
    629 	m.Logf(LogDebug, "Order status for %s is %s", pendingEntry.HashedSigningKey, orderStatus)
    630 	if merchant.OrderPaid == orderStatus {
    631 		m.Logf(LogDebug, "Order for %v appears to be paid", pendingEntry)
    632 		err = m.DB.GetMailboxMetadataBySigningKey(&registrationEntry, hMailbox)
    633 		if err == nil {
    634 			m.Logf(LogDebug, "Adding %d seconds to entry expiration", pendingEntry.Duration)
    635 			registrationEntry.Expiration.Seconds += uint64(pendingEntry.Duration)
    636 			err = m.DB.UpdateMailboxExpiration(&registrationEntry)
    637 			if nil != err {
    638 				m.Logf(LogInfo, "Error updating mailbox expiration: %v\n", err)
    639 			}
    640 			_, err = m.DB.DeletePendingRegistration(&pendingEntry)
    641 			if nil != err {
    642 				m.Logf(LogInfo, "Error deleting pending registration: %v\n", err)
    643 			}
    644 		}
    645 		return
    646 	}
    647 }
    648 
    649 // deleteMessagesResponse deletes messages from a mailbox.
    650 //
    651 // @Summary     Delete messages
    652 // @Description Deletes one or more messages starting from the serial given in the If-Match header.
    653 // @Description Requires a valid EdDSA signature in the Taler-Mailbox-Delete-Signature header.
    654 // @Tags        mailbox
    655 // @Param       mailbox    path   string true  "Crockford base32-encoded EdDSA public key of the mailbox"
    656 // @Param       count      query  int    false "Number of messages to delete (default: 1)"
    657 // @Param       If-Match   header string true  "Serial number of the first message to delete"
    658 // @Param       Taler-Mailbox-Delete-Signature header string true "EdDSA signature authorising the deletion"
    659 // @Success     204 "Messages deleted"
    660 // @Failure     400 "Missing or malformed headers/parameters"
    661 // @Failure     403 "Signature invalid"
    662 // @Failure     404 "Message with given serial not found"
    663 // @Failure     500
    664 // @Router      /{mailbox} [delete]
    665 func (m *Mailbox) deleteMessagesResponse(w http.ResponseWriter, r *http.Request) {
    666 	var errDetail ErrorDetail
    667 	vars := mux.Vars(r)
    668 	etagHeader := r.Header.Get("If-Match")
    669 	if etagHeader == "" {
    670 		http.Error(w, "If-Match header missing", http.StatusBadRequest)
    671 		return
    672 	}
    673 	if strings.Contains(etagHeader, ",") {
    674 		http.Error(w, "If-Match contains multiple values", http.StatusBadRequest)
    675 		return
    676 	}
    677 	expectedETag, err := strconv.Atoi(etagHeader)
    678 	if err != nil {
    679 		http.Error(w, "If-Match contains malformed etag number", http.StatusBadRequest)
    680 		return
    681 	}
    682 	pkey, err := util.Base32CrockfordDecode(vars["mailbox"], 32)
    683 	if err != nil {
    684 		http.Error(w, "Parameter encoding invalid.", http.StatusBadRequest)
    685 		return
    686 	}
    687 	count := 1
    688 	countStr := r.URL.Query().Get("count")
    689 	if len(countStr) > 0 {
    690 		count, err = strconv.Atoi(countStr)
    691 		if err != nil {
    692 			http.Error(w, "Malformed count parameter", http.StatusBadRequest)
    693 			w.WriteHeader(http.StatusBadRequest)
    694 			return
    695 		}
    696 	}
    697 	headerSig := r.Header["Taler-Mailbox-Delete-Signature"]
    698 	if nil == headerSig {
    699 		http.Error(w, "Missing signature", http.StatusBadRequest)
    700 		return
    701 	}
    702 	pk := ed25519.PublicKey(pkey)
    703 	sig, err := util.Base32CrockfordDecode(headerSig[0], 64)
    704 	if nil != err {
    705 		w.WriteHeader(http.StatusBadRequest)
    706 		return
    707 	}
    708 	h := sha512.New()
    709 	h.Write(pkey)
    710 	hMailbox := util.Base32CrockfordEncode(h.Sum(nil))
    711 	m.checkPendingMailboxRegistrationUpdates(hMailbox)
    712 	var signedMsg [4 * 4]byte
    713 	binary.BigEndian.PutUint32(signedMsg[0:4], 4*4)
    714 	binary.BigEndian.PutUint32(signedMsg[4:8], gana.TalerSignaturePurposeMailboxMessagesDelete)
    715 	binary.BigEndian.PutUint32(signedMsg[8:12], uint32(expectedETag))
    716 	binary.BigEndian.PutUint32(signedMsg[12:16], uint32(count))
    717 	if !ed25519.Verify(pk, signedMsg[0:], sig) {
    718 		errDetail.Code = gana.WALLET_MAILBOX_MESSAGE_DELETE_DENIED
    719 		errDetail.Hint = "Signature invalid."
    720 		resp, _ := json.Marshal(errDetail)
    721 		w.WriteHeader(http.StatusForbidden)
    722 		w.Write(resp)
    723 		return
    724 	}
    725 	// Check that expectedETag actually exists
    726 	var entry InboxEntry
    727 	err = m.DB.GetInboxEntryBySerial(&entry, hMailbox, int64(expectedETag))
    728 	if err != nil {
    729 		m.Logf(LogDebug, "Message to delete not found with ID %d", expectedETag)
    730 		errDetail.Code = gana.WALLET_MAILBOX_MESSAGE_DELETE_FAILED
    731 		errDetail.Hint = "Message to delete not found."
    732 		resp, _ := json.Marshal(errDetail)
    733 		w.Write(resp)
    734 		w.WriteHeader(http.StatusNotFound)
    735 		return
    736 	}
    737 	m.Logf(LogError, "Deleting from entry %v up to %d messages\n", entry, count)
    738 	num, err := m.DB.DeleteInboxEntryBySerial(&entry, count)
    739 	if err != nil {
    740 		m.Logf(LogDebug, "Failed to delete messages: %v", err)
    741 		w.WriteHeader(http.StatusInternalServerError)
    742 		return
    743 	}
    744 	m.Logf(LogDebug, "Found matching ID (serial: %d), deleted %d messages", entry.Serial, num)
    745 	w.WriteHeader(http.StatusNoContent)
    746 }
    747 
    748 func (m *Mailbox) termsResponse(w http.ResponseWriter, r *http.Request) {
    749 	termspath := m.Cfg.Ini.GetFilename("mailbox", "default_terms_path", "terms/", m.Cfg.Datahome)
    750 	tos.ServiceTermsResponse(w, r, termspath, tos.TalerTosConfig{
    751 		DefaultFileType:    m.Cfg.Ini.GetString("mailbox", "default_doc_filetype", "text/html"),
    752 		DefaultLanguage:    m.Cfg.Ini.GetString("mailbox", "default_doc_lang", "en"),
    753 		SupportedFileTypes: strings.Split(m.Cfg.Ini.GetString("mailbox", "supported_doc_filetypes", ""), " "),
    754 	})
    755 }
    756 
    757 func (m *Mailbox) privacyResponse(w http.ResponseWriter, r *http.Request) {
    758 	pppath := m.Cfg.Ini.GetFilename("mailbox", "default_pp_path", "privacy/", m.Cfg.Datahome)
    759 	tos.PrivacyPolicyResponse(w, r, pppath, tos.TalerTosConfig{
    760 		DefaultFileType:    m.Cfg.Ini.GetString("mailbox", "default_doc_filetype", "text/html"),
    761 		DefaultLanguage:    m.Cfg.Ini.GetString("mailbox", "default_doc_lang", "en"),
    762 		SupportedFileTypes: strings.Split(m.Cfg.Ini.GetString("mailbox", "supported_doc_filetypes", ""), " "),
    763 	})
    764 }
    765 
    766 func (m *Mailbox) setupHandlers() {
    767 	m.Router = mux.NewRouter().StrictSlash(true)
    768 
    769 	/* ToS API */
    770 	m.Router.HandleFunc("/terms", m.termsResponse).Methods("GET")
    771 	m.Router.HandleFunc("/privacy", m.privacyResponse).Methods("GET")
    772 
    773 	/* Config API */
    774 	m.Router.HandleFunc("/config", m.configResponse).Methods("GET")
    775 
    776 	/* Mailbox API */
    777 	m.Router.HandleFunc("/register", m.registerMailboxResponse).Methods("POST")
    778 	m.Router.HandleFunc("/info/{h_mailbox}", m.getKeysResponse).Methods("GET")
    779 	m.Router.HandleFunc("/{h_mailbox}", m.sendMessageResponse).Methods("POST")
    780 	m.Router.HandleFunc("/{h_mailbox}", m.getMessagesResponse).Methods("GET")
    781 	m.Router.HandleFunc("/{mailbox}", m.deleteMessagesResponse).Methods("DELETE")
    782 }
    783 
    784 func (m *Mailbox) Logf(loglevel LogLevel, fmt string, args ...any) {
    785 	if loglevel < m.Cfg.Loglevel {
    786 		return
    787 	}
    788 	m.Logger.SetPrefix("taler-mailbox - " + LoglevelStringMap[loglevel] + " ")
    789 	m.Logger.Printf(fmt, args...)
    790 }
    791 
    792 // Initialize the Mailbox instance with cfgfile
    793 func (m *Mailbox) Initialize(cfg MailboxConfig) {
    794 	m.Cfg = cfg
    795 	m.Logger = log.New(os.Stdout, "taler-mailbox:", log.LstdFlags)
    796 	m.BaseURL = cfg.Ini.GetString("mailbox", "base_url", "https://example.com")
    797 	m.MessageBodyBytes = cfg.Ini.GetInt64("mailbox", "message_body_bytes", 256)
    798 	m.MessageResponseLimit = uint64(cfg.Ini.GetInt64("mailbox", "message_response_limit", 50))
    799 	monthlyFee, err := cfg.Ini.GetAmount("mailbox", "monthly_fee", &talerutil.Amount{})
    800 	if err != nil {
    801 		fmt.Printf("Failed to parse monthly fee: %v", err)
    802 		os.Exit(1)
    803 	}
    804 	m.MonthlyFee = monthlyFee
    805 	updateFee, err := cfg.Ini.GetAmount("mailbox", "registration_update_fee", &talerutil.Amount{})
    806 	if err != nil {
    807 		fmt.Printf("Failed to parse update fee: %v", err)
    808 		os.Exit(1)
    809 	}
    810 	m.RegistrationUpdateFee = updateFee
    811 	messageFee, err := cfg.Ini.GetAmount("mailbox", "message_fee", &talerutil.Amount{})
    812 	if err != nil {
    813 		fmt.Printf("Failed to parse message fee: %v", err)
    814 		os.Exit(1)
    815 	}
    816 	m.MessageFee = messageFee
    817 	m.FreeMessageQuota = uint64(cfg.Ini.GetInt64("mailbox", "free_message_quota", 0))
    818 	m.DB = cfg.DB
    819 	go func() {
    820 		for {
    821 			num, err := m.DB.DeleteStaleRegistrations(time.Now())
    822 			if err != nil {
    823 				m.Logf(LogDebug, "Error purging stale registrations: `%v'.\n", err)
    824 			}
    825 			m.Logf(LogInfo, "Cleaned up %d stale registrations.\n", num)
    826 			time.Sleep(time.Hour * 24)
    827 		}
    828 	}()
    829 	// Clean up pending
    830 	pendingExp, err := cfg.Ini.GetDuration("mailbox", "pending_registration_expiration", 24*time.Hour)
    831 	if err != nil {
    832 		fmt.Printf("Failed to parse pending registration expiration: %v", err)
    833 		os.Exit(1)
    834 	}
    835 	go func() {
    836 		for {
    837 			num, err := m.DB.DeleteStalePendingRegistrations(time.Now().Add(-pendingExp))
    838 			if err != nil {
    839 				m.Logf(LogDebug, "Error purging stale registrations: `%v'.\n", err)
    840 			}
    841 			m.Logf(LogInfo, "Cleaned up %d stale pending registrations.\n", num)
    842 			time.Sleep(pendingExp)
    843 		}
    844 	}()
    845 
    846 	m.Merchant = cfg.Merchant
    847 	if !monthlyFee.IsZero() {
    848 		merchConfig, err := m.Merchant.GetConfig()
    849 		if err != nil {
    850 			fmt.Printf("Failed to get merchant config: %v", err)
    851 			os.Exit(1)
    852 		}
    853 		currencySpec, currencySupported := merchConfig.Currencies[monthlyFee.Currency]
    854 		for !currencySupported {
    855 			fmt.Printf("Currency `%s' not supported by merchant!\n", monthlyFee.Currency)
    856 			os.Exit(1)
    857 		}
    858 		m.CurrencySpec = currencySpec
    859 	}
    860 	m.setupHandlers()
    861 }