cashless2ecash

cashless2ecash: pay with cards for digital cash (experimental)
Log | Files | Refs | README

cli.go (15586B)


      1 package main
      2 
      3 import (
      4 	"bufio"
      5 	"bytes"
      6 	"context"
      7 	"crypto/rand"
      8 	"encoding/base64"
      9 	"errors"
     10 	"fmt"
     11 	"os"
     12 	"strconv"
     13 	"strings"
     14 	"time"
     15 
     16 	"github.com/jackc/pgx/v5"
     17 	"github.com/jackc/pgx/v5/pgxpool"
     18 	"golang.org/x/crypto/argon2"
     19 	"gopkg.in/ini.v1"
     20 )
     21 
     22 const ACTION_HELP = "h"
     23 const ACTION_SETUP_SIMULATION = "sim"
     24 const ACTION_REGISTER_PROVIDER = "rp"
     25 const ACTION_REGISTER_TERMINAL = "rt"
     26 const ACTION_DEACTIVATE_TERMINAL = "dt"
     27 const ACTION_ACTIVATE_TERMINAL = "at"
     28 const ACTION_WITHDRAWAL_INFOMRATION = "w"
     29 const ACTION_WITHDRAWAL_INFOMRATION_BY_PTID = "wp"
     30 const ACTION_PROVIDER_CREDENTIALS = "wc"
     31 const ACTION_CONNECT_DB = "db"
     32 const ACTION_QUIT = "q"
     33 
     34 // format of wallee backend credentials.
     35 type WalleeCredentials struct {
     36 	SpaceId            int    `json:"spaceId"`
     37 	UserId             int    `json:"userId"`
     38 	ApplicationUserKey string `json:"application-user-key"`
     39 }
     40 
     41 var DB *pgxpool.Pool
     42 
     43 // enter database credentials (host, port, database, username, password)
     44 // register terminal -> read password, hash, save to database
     45 // register provider -> read wallee, read space(id), read userid, read password, hash what needs to be hashed, save to database
     46 
     47 func main() {
     48 
     49 	iniFilePath := ""
     50 	connstr := ""
     51 	if len(os.Args) > 1 {
     52 
     53 		nextIsConf := false
     54 		for i, arg := range os.Args {
     55 			if i == 0 {
     56 				continue
     57 			} else if nextIsConf {
     58 				iniFilePath = arg
     59 				nextIsConf = false
     60 			} else if arg == "-c" {
     61 				nextIsConf = true
     62 			} else if arg == "-h" {
     63 				showHelp()
     64 				os.Exit(0)
     65 			}
     66 		}
     67 	}
     68 
     69 	if iniFilePath != "" {
     70 		optionalPgConnStr, err := parseDbConnstrFromIni(iniFilePath)
     71 		if err != nil {
     72 			fmt.Println("failed parsing config:", err.Error())
     73 		}
     74 		fmt.Println("read connection string from ini:", optionalPgConnStr)
     75 		connstr = optionalPgConnStr
     76 	}
     77 
     78 	if connstr != "" {
     79 		err := connectDbUsingString(connstr)
     80 		if err != nil {
     81 			fmt.Println("error while connecting to database, using connection string from config. error:", err.Error())
     82 		}
     83 	}
     84 	fmt.Println("What do you want to do?")
     85 	showHelp()
     86 	for {
     87 		err := dispatchCommand(read("Type command (term in brackets): "))
     88 		if err != nil {
     89 			fmt.Println("Error occured:", err.Error())
     90 		}
     91 	}
     92 }
     93 
     94 func parseDbConnstrFromIni(path string) (string, error) {
     95 
     96 	f, err := os.Open(path)
     97 	if err != nil {
     98 		return "", err
     99 	}
    100 	defer f.Close()
    101 
    102 	stat, err := f.Stat()
    103 	if err != nil {
    104 		return "", err
    105 	}
    106 
    107 	content := make([]byte, stat.Size())
    108 	_, err = f.Read(content)
    109 	if err != nil {
    110 		return "", err
    111 	}
    112 
    113 	ini, err := ini.Load(content)
    114 	if err != nil {
    115 		return "", err
    116 	}
    117 
    118 	section := ini.Section("database")
    119 	if section == nil {
    120 		fmt.Println("database section not configured in ini file")
    121 		os.Exit(0)
    122 	}
    123 
    124 	value, err := section.GetKey("CONFIG")
    125 	if err != nil {
    126 		return "", err
    127 	}
    128 
    129 	return value.String(), nil
    130 }
    131 
    132 func walleePrepareCredentials() (string, string, string, string, error) {
    133 
    134 	name := "Wallee"
    135 	paytotargettype := "wallee-transaction"
    136 	backendUrl := read("Wallee backend base url: ")
    137 	spaceIdStr := read("Wallee Space Id: ")
    138 	spaceId, err := strconv.Atoi(spaceIdStr)
    139 	if err != nil {
    140 		return "", "", "", "", err
    141 	}
    142 	userIdStr := read("Wallee User Id: ")
    143 	userId, err := strconv.Atoi(userIdStr)
    144 	if err != nil {
    145 		return "", "", "", "", err
    146 	}
    147 	key := read("Wallee Application User Key: ")
    148 	hashedKey, err := pbkdf(key)
    149 	if err != nil {
    150 		return "", "", "", "", err
    151 	}
    152 
    153 	creds, err := NewJsonCodec[WalleeCredentials]().EncodeToBytes(&WalleeCredentials{
    154 		SpaceId:            spaceId,
    155 		UserId:             userId,
    156 		ApplicationUserKey: hashedKey,
    157 	})
    158 	if err != nil {
    159 		return "", "", "", "", err
    160 	}
    161 	credsEncoded := base64.StdEncoding.EncodeToString(creds)
    162 
    163 	return name, paytotargettype, backendUrl, credsEncoded, nil
    164 }
    165 
    166 func generateProviderCredentials() error {
    167 
    168 	name, paytottargettype, backendUrl, credsEncoded, err := walleePrepareCredentials()
    169 	if err != nil {
    170 		return err
    171 	}
    172 
    173 	fmt.Println("provider-name:", name)
    174 	fmt.Println("provider-payto-target-type:", paytottargettype)
    175 	fmt.Println("provider-backend-url:", backendUrl)
    176 	fmt.Println("base64 encoded credentials (can be added to the database like this):", credsEncoded)
    177 
    178 	return nil
    179 }
    180 
    181 func registerWalleeProvider() error {
    182 
    183 	if DB == nil {
    184 		return errors.New("connect to the database first (cmd: db)")
    185 	}
    186 
    187 	name, paytotargettype, backendUrl, credsEncoded, err := walleePrepareCredentials()
    188 	if err != nil {
    189 		return err
    190 	}
    191 
    192 	_, err = DB.Exec(
    193 		context.Background(),
    194 		INSERT_PROVIDER,
    195 		name,
    196 		paytotargettype,
    197 		backendUrl,
    198 		credsEncoded,
    199 	)
    200 	if err != nil {
    201 		return err
    202 	}
    203 
    204 	return nil
    205 }
    206 
    207 func registerWalleeTerminal() error {
    208 
    209 	if DB == nil {
    210 		return errors.New("connect to the database first (cmd: db)")
    211 	}
    212 
    213 	description := read("Description (location, inventory identifier, etc.): ")
    214 	providerName := read("Provider Name: ")
    215 
    216 	rows, err := DB.Query(
    217 		context.Background(),
    218 		GET_PROVIDER_BY_NAME,
    219 		providerName,
    220 	)
    221 	if err != nil {
    222 		return err
    223 	}
    224 
    225 	p, err := pgx.CollectOneRow(rows, pgx.RowToAddrOfStructByName[Provider])
    226 	if err != nil {
    227 		return err
    228 	}
    229 	fmt.Println("Collected provider by name")
    230 	rows.Close() // release rows / connection
    231 
    232 	accessToken := make([]byte, 32)
    233 	_, err = rand.Read(accessToken)
    234 	if err != nil {
    235 		return err
    236 	}
    237 
    238 	accessTokenBase64 := base64.StdEncoding.EncodeToString(accessToken)
    239 
    240 	hashedAccessToken, err := pbkdf(accessTokenBase64)
    241 	if err != nil {
    242 		return err
    243 	}
    244 
    245 	fmt.Println("adding terminal")
    246 	_, err = DB.Exec(
    247 		context.Background(),
    248 		INSERT_TERMINAL,
    249 		hashedAccessToken,
    250 		description,
    251 		p.ProviderId,
    252 	)
    253 	if err != nil {
    254 		return err
    255 	}
    256 
    257 	fmt.Println("looking up last inserted terminal")
    258 	rows, err = DB.Query(
    259 		context.Background(),
    260 		GET_LAST_INSERTED_TERMINAL,
    261 	)
    262 	if err != nil {
    263 		return err
    264 	}
    265 	t, err := pgx.CollectOneRow(rows, pgx.RowToAddrOfStructByName[Terminal])
    266 	if err != nil {
    267 		return err
    268 	}
    269 	rows.Close()
    270 
    271 	fmt.Println("Terminal-User-Id (used to identify terminal at the api. You want to note this):", "Wallee-"+strconv.Itoa(int(t.TerminalID)))
    272 	fmt.Println("GENERATED ACCESS-TOKEN (save it in your password manager. Can't be recovered!!):")
    273 	fmt.Println(accessTokenBase64)
    274 
    275 	return nil
    276 }
    277 
    278 func deactivateTerminal() error {
    279 
    280 	if DB == nil {
    281 		return errors.New("connect to the database first (cmd: db)")
    282 	}
    283 
    284 	fmt.Println("You are about to deactivate terminal which allows withdrawals. This will make the terminal unusable.")
    285 	tuid := read("Terminal-User-Id: ")
    286 	parts := strings.Split(tuid, "-")
    287 	if len(parts) != 2 {
    288 		return errors.New("invalid terminal-user-id (format=[PROVIDER_NAME]-[NUMBER])")
    289 	}
    290 	tid, err := strconv.Atoi(parts[1])
    291 	if err != nil {
    292 		return err
    293 	}
    294 
    295 	_, err = DB.Exec(
    296 		context.Background(),
    297 		DEACTIVATE_TERMINAL,
    298 		tid,
    299 	)
    300 	if err != nil {
    301 		return err
    302 	}
    303 
    304 	return nil
    305 }
    306 
    307 func activateTerminal() error {
    308 
    309 	if DB == nil {
    310 		return errors.New("connect to the database first (cmd: db)")
    311 	}
    312 
    313 	fmt.Println("You are about to activate a terminal which allows withdrawals. This will make the terminal operational.")
    314 	tuid := read("Terminal-User-Id: ")
    315 	parts := strings.Split(tuid, "-")
    316 	if len(parts) != 2 {
    317 		return errors.New("invalid terminal-user-id (format=[PROVIDER_NAME]-[NUMBER])")
    318 	}
    319 	tid, err := strconv.Atoi(parts[1])
    320 	if err != nil {
    321 		return err
    322 	}
    323 
    324 	_, err = DB.Exec(
    325 		context.Background(),
    326 		ACTIVATE_TERMINAL,
    327 		tid,
    328 	)
    329 	if err != nil {
    330 		return err
    331 	}
    332 
    333 	return nil
    334 }
    335 
    336 func withdrawalInformationByWopid() error {
    337 
    338 	if DB == nil {
    339 		return errors.New("connect to the database first (cmd: db)")
    340 	}
    341 
    342 	wopid := read("WOPID (encoded): ")
    343 	wopidDecoded, err := decodeCrock(wopid)
    344 	if err != nil {
    345 		return err
    346 	}
    347 	rows, err := DB.Query(
    348 		context.Background(),
    349 		GET_WITHDRAWAL_BY_WOPID,
    350 		wopidDecoded,
    351 	)
    352 	if err != nil {
    353 		return err
    354 	}
    355 
    356 	return readPrintWithdrawal(rows)
    357 }
    358 
    359 func withdrawalInformationByProviderTransactionId() error {
    360 
    361 	if DB == nil {
    362 		return errors.New("connect to the database first (cmd: db)")
    363 	}
    364 
    365 	ptid := read("Provider Transaction ID: ")
    366 	rows, err := DB.Query(
    367 		context.Background(),
    368 		GET_WITHDRAWAL_BY_PROVIDER_TRANSACTION_ID,
    369 		ptid,
    370 	)
    371 	if err != nil {
    372 		return err
    373 	}
    374 
    375 	return readPrintWithdrawal(rows)
    376 }
    377 
    378 func readPrintWithdrawal(rows pgx.Rows) error {
    379 
    380 	type TalerAmountCurrency struct {
    381 		Val  int64  `db:"val"`
    382 		Frac int32  `db:"frac"`
    383 		Curr string `db:"curr"`
    384 	}
    385 	type Withdrawal struct {
    386 		WithdrawalRowId       uint64               `db:"withdrawal_row_id"`
    387 		ConfirmedRowId        uint64               `db:"confirmed_row_id"`
    388 		RequestUid            string               `db:"request_uid"`
    389 		Wopid                 []byte               `db:"wopid"`
    390 		ReservePubKey         []byte               `db:"reserve_pub_key"`
    391 		RegistrationTs        int64                `db:"registration_ts"`
    392 		Amount                *TalerAmountCurrency `db:"amount" scan:"follow"`
    393 		SuggestedAmount       *TalerAmountCurrency `db:"suggested_amount" scan:"follow"`
    394 		TerminalFees          *TalerAmountCurrency `db:"terminal_fees" scan:"follow"`
    395 		WithdrawalStatus      string               `db:"withdrawal_status"`
    396 		TerminalId            int                  `db:"terminal_id"`
    397 		ProviderTransactionId *string              `db:"provider_transaction_id"`
    398 		LastRetryTs           *int64               `db:"last_retry_ts"`
    399 		RetryCounter          int32                `db:"retry_counter"`
    400 		CompletionProof       []byte               `db:"completion_proof"`
    401 	}
    402 
    403 	w, err := pgx.CollectOneRow(rows, pgx.RowToAddrOfStructByName[Withdrawal])
    404 	if err != nil {
    405 		return err
    406 	}
    407 	rows.Close()
    408 
    409 	indent := " -"
    410 	fmt.Println("Withdrawal:")
    411 	fmt.Println(indent, "wopid             :", encodeCrock(w.Wopid))
    412 	fmt.Println(indent, "status            :", w.WithdrawalStatus)
    413 	fmt.Println(indent, "reserve public key:", encodeCrock(w.ReservePubKey))
    414 	fmt.Println(indent, "provider tid      :", *w.ProviderTransactionId)
    415 	fmt.Println(indent, "amount            :", w.Amount)
    416 	fmt.Println(indent, "terminal          :", w.TerminalId)
    417 	fmt.Println(indent, "attest retries    :", w.RetryCounter)
    418 	if w.LastRetryTs != nil {
    419 		fmt.Println(indent, "last retry        :", time.Unix(*w.LastRetryTs, 0).Format("yyyy-MM-dd hh:mm:ss"))
    420 	}
    421 
    422 	return nil
    423 }
    424 
    425 func setupSimulation() error {
    426 
    427 	if DB == nil {
    428 		return errors.New("connect to the database first (cmd: db)")
    429 	}
    430 
    431 	// SETTING UP PROVIDER
    432 	fmt.Println("Setting up simulation provider and terminal.")
    433 	name := "Simulation"
    434 	paytotargettype := "void"
    435 	backendUrl := "simulation provider will not contact any backend."
    436 	credsEncoded := base64.StdEncoding.EncodeToString(bytes.NewBufferString("simulation provider will not contact any backend.").Bytes())
    437 
    438 	_, err := DB.Exec(
    439 		context.Background(),
    440 		INSERT_PROVIDER,
    441 		name,
    442 		paytotargettype,
    443 		backendUrl,
    444 		credsEncoded,
    445 	)
    446 	if err != nil {
    447 		return err
    448 	}
    449 
    450 	// SETTING UP TERMINAL
    451 	description := "simulation terminal"
    452 
    453 	rows, err := DB.Query(
    454 		context.Background(),
    455 		GET_PROVIDER_BY_NAME,
    456 		name,
    457 	)
    458 	if err != nil {
    459 		return err
    460 	}
    461 
    462 	p, err := pgx.CollectOneRow(rows, pgx.RowToAddrOfStructByName[Provider])
    463 	if err != nil {
    464 		return err
    465 	}
    466 	rows.Close() // release rows / connection
    467 
    468 	accessToken := make([]byte, 32)
    469 	_, err = rand.Read(accessToken)
    470 	if err != nil {
    471 		return err
    472 	}
    473 
    474 	accessTokenBase64 := base64.StdEncoding.EncodeToString(accessToken)
    475 
    476 	hashedAccessToken, err := pbkdf(accessTokenBase64)
    477 	if err != nil {
    478 		return err
    479 	}
    480 
    481 	_, err = DB.Exec(
    482 		context.Background(),
    483 		INSERT_TERMINAL,
    484 		hashedAccessToken,
    485 		description,
    486 		p.ProviderId,
    487 	)
    488 	if err != nil {
    489 		return err
    490 	}
    491 
    492 	fmt.Println("looking up last inserted terminal")
    493 	rows, err = DB.Query(
    494 		context.Background(),
    495 		GET_LAST_INSERTED_TERMINAL,
    496 	)
    497 	if err != nil {
    498 		return err
    499 	}
    500 	t, err := pgx.CollectOneRow(rows, pgx.RowToAddrOfStructByName[Terminal])
    501 	if err != nil {
    502 		return err
    503 	}
    504 	rows.Close()
    505 
    506 	fmt.Println("Terminal-User-Id (used to identify terminal at the api. You want to note this):", name+"-"+strconv.Itoa(int(t.TerminalID)))
    507 	fmt.Println("GENERATED ACCESS-TOKEN (save it in your password manager. Can't be recovered!!):")
    508 	fmt.Println(accessTokenBase64)
    509 
    510 	return nil
    511 }
    512 
    513 func connectDatabase() error {
    514 
    515 	u := read("Username: ")
    516 	pw := read("Password: ")
    517 	h := read("Host: ")
    518 	ps := read("Port: ")
    519 	p, err := strconv.Atoi(ps)
    520 	if err != nil {
    521 		return err
    522 	}
    523 	d := read("Database: ")
    524 
    525 	connstring := PostgresConnectionString(u, pw, h, p, d)
    526 	return connectDbUsingString(connstring)
    527 }
    528 
    529 func connectDbUsingString(connString string) error {
    530 	dbCfg, err := pgxpool.ParseConfig(connString)
    531 	if err != nil {
    532 		return err
    533 	}
    534 
    535 	dbCfg.AfterConnect = registerCustomTypesHook
    536 	DB, err = pgxpool.NewWithConfig(context.Background(), dbCfg)
    537 	if err != nil {
    538 		return err
    539 	}
    540 	fmt.Println("connected to database")
    541 	return nil
    542 }
    543 
    544 func showHelp() error {
    545 
    546 	fmt.Println("register wallee provider (", ACTION_REGISTER_PROVIDER, ")")
    547 	fmt.Println("register wallee terminal (", ACTION_REGISTER_TERMINAL, ")")
    548 	fmt.Println("deactivate wallee terminal (", ACTION_DEACTIVATE_TERMINAL, ")")
    549 	fmt.Println("activate wallee terminal (", ACTION_ACTIVATE_TERMINAL, ")")
    550 	fmt.Println("setup simulation (", ACTION_SETUP_SIMULATION, ")")
    551 	fmt.Println("withdrawal information by wopid (", ACTION_WITHDRAWAL_INFOMRATION, ")")
    552 	fmt.Println("witdhrawal information by provider transaction id (", ACTION_WITHDRAWAL_INFOMRATION_BY_PTID, ")")
    553 	fmt.Println("create wallee provider credentials string (", ACTION_PROVIDER_CREDENTIALS, ")")
    554 	if DB == nil {
    555 		fmt.Println("connect database (", ACTION_CONNECT_DB, ")")
    556 	}
    557 	fmt.Println("show help (", ACTION_HELP, ")")
    558 	fmt.Println("quit (", ACTION_QUIT, ")")
    559 	return nil
    560 }
    561 
    562 func quit() error {
    563 	fmt.Println("bye...")
    564 	os.Exit(0)
    565 	return nil
    566 }
    567 
    568 func pbkdf(pw string) (string, error) {
    569 
    570 	rfcTime := 3
    571 	rfcMemory := 32 * 1024
    572 	salt := make([]byte, 16)
    573 	_, err := rand.Read(salt)
    574 	if err != nil {
    575 		return "", err
    576 	}
    577 	key := argon2.Key([]byte(pw), salt, uint32(rfcTime), uint32(rfcMemory), 4, 32)
    578 
    579 	keyAndSalt := make([]byte, 0, 48)
    580 	keyAndSalt = append(keyAndSalt, key...)
    581 	keyAndSalt = append(keyAndSalt, salt...)
    582 	if len(keyAndSalt) != 48 {
    583 		return "", errors.New("invalid password hash and salt")
    584 	}
    585 	return base64.StdEncoding.EncodeToString(keyAndSalt), nil
    586 }
    587 
    588 func PostgresConnectionString(
    589 	username string,
    590 	password string,
    591 	host string,
    592 	port int,
    593 	database string,
    594 ) string {
    595 	return fmt.Sprintf(
    596 		"postgres://%s:%s@%s:%d/%s",
    597 		username,
    598 		password,
    599 		host,
    600 		port,
    601 		database,
    602 	)
    603 }
    604 
    605 func dispatchCommand(cmd string) error {
    606 
    607 	var err error
    608 	switch cmd {
    609 	case ACTION_HELP:
    610 		err = showHelp()
    611 	case ACTION_QUIT:
    612 		err = quit()
    613 	case ACTION_CONNECT_DB:
    614 		err = connectDatabase()
    615 	case ACTION_REGISTER_PROVIDER:
    616 		err = registerWalleeProvider()
    617 	case ACTION_REGISTER_TERMINAL:
    618 		err = registerWalleeTerminal()
    619 	case ACTION_DEACTIVATE_TERMINAL:
    620 		err = deactivateTerminal()
    621 	case ACTION_ACTIVATE_TERMINAL:
    622 		err = activateTerminal()
    623 	case ACTION_WITHDRAWAL_INFOMRATION:
    624 		err = withdrawalInformationByWopid()
    625 	case ACTION_WITHDRAWAL_INFOMRATION_BY_PTID:
    626 		err = withdrawalInformationByProviderTransactionId()
    627 	case ACTION_PROVIDER_CREDENTIALS:
    628 		err = generateProviderCredentials()
    629 	case ACTION_SETUP_SIMULATION:
    630 		err = setupSimulation()
    631 	default:
    632 		fmt.Println("unknown action")
    633 	}
    634 	return err
    635 }
    636 
    637 func read(prefix string) string {
    638 	reader := bufio.NewReader(os.Stdin)
    639 	fmt.Print(prefix)
    640 	inp, err := reader.ReadString('\n')
    641 	if err != nil {
    642 		fmt.Println(err.Error())
    643 		return ""
    644 	}
    645 	return strings.Trim(inp, "\n")
    646 }
    647 
    648 func registerCustomTypesHook(ctx context.Context, conn *pgx.Conn) error {
    649 
    650 	t, err := conn.LoadType(ctx, "c2ec.taler_amount_currency")
    651 	if err != nil {
    652 		return err
    653 	}
    654 
    655 	conn.TypeMap().RegisterType(t)
    656 	return nil
    657 }