commit 76246c338abf6470313267b0d1c2df6798ba4d6d parent fd19956fd52fbab0c922cd58fac66cad97a94805 Author: Joel-Haeberli <haebu@rubigen.ch> Date: Wed, 24 Apr 2024 21:33:30 +0200 fix: crockford encoding Diffstat:
21 files changed, 865 insertions(+), 364 deletions(-)
diff --git a/c2ec/auth.go b/c2ec/auth.go @@ -86,28 +86,6 @@ func AuthenticateTerminal(req *http.Request) bool { return false } -// find out how the wallet authenticates itself. -// returns true if authentication was successful, otherwise false -// when not successful, the api shall return immediately -func AuthenticateWallet(req *http.Request) bool { - - // Is this needed? Understand how the wallet authenticates itself at the exchange currently first. - // https://docs.taler.net/design-documents/049-auth.html#dd48-token - // https://docs.taler.net/core/api-corebank.html#authentication - // - // /accounts/$USERNAME/token - // - // The username in our case is the reserve public key - // registered for withdrawal. At the initial registration - // of the reserve public key we leverage a TOFU trust model. - // during the registration of the reserve public key a new - // access token will be created with a limited lifetime. - // The token will not be refreshable and become invalid - // only after a few minutes. Since the Wallet will register - // a wopid and - return true -} - func parseBasicAuth(basicAuth string) (string, string, error) { parts := strings.Split(basicAuth, ":") diff --git a/c2ec/bank-integration.go b/c2ec/bank-integration.go @@ -10,14 +10,12 @@ import ( "time" ) -const BANK_INTEGRATION_CONFIG_ENDPOINT = "/config" const WITHDRAWAL_OPERATION = "/withdrawal-operation" const WOPID_PARAMETER = "wopid" -const BANK_INTEGRATION_CONFIG_PATTERN = BANK_INTEGRATION_CONFIG_ENDPOINT +const BANK_INTEGRATION_CONFIG_PATTERN = "/config" const WITHDRAWAL_OPERATION_PATTERN = WITHDRAWAL_OPERATION const WITHDRAWAL_OPERATION_BY_WOPID_PATTERN = WITHDRAWAL_OPERATION + "/{" + WOPID_PARAMETER + "}" -const WITHDRAWAL_OPERATION_PAYMENT_PATTERN = WITHDRAWAL_OPERATION_BY_WOPID_PATTERN + "/confirm" const WITHDRAWAL_OPERATION_ABORTION_PATTERN = WITHDRAWAL_OPERATION_BY_WOPID_PATTERN + "/abort" const DEFAULT_LONG_POLL_MS = 1000 @@ -80,63 +78,6 @@ func bankIntegrationConfig(res http.ResponseWriter, req *http.Request) { res.Write(serializedCfg) } -func handleWithdrawalRegistration(res http.ResponseWriter, req *http.Request) { - - jsonCodec := NewJsonCodec[C2ECWithdrawRegistration]() - registration, err := ReadStructFromBody(req, jsonCodec) - if err != nil { - LogWarn("bank-integration-api", fmt.Sprintf("invalid body for withdrawal registration error=%s", err.Error())) - err := WriteProblem(res, HTTP_BAD_REQUEST, &RFC9457Problem{ - TypeUri: TALER_URI_PROBLEM_PREFIX + "/C2EC_WITHDRAW_REGISTRATION_INVALID_REQ", - Title: "invalid request", - Detail: "the registration request for the withdrawal is malformed (error: " + err.Error() + ")", - Instance: req.RequestURI, - }) - if err != nil { - res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR) - } - return - } - - // read and validate the wopid path parameter - wopid := req.PathValue(WOPID_PARAMETER) - wpd, err := ParseWopid(wopid) - if err != nil { - LogWarn("bank-integration-api", "wopid "+wopid+" not valid") - err := WriteProblem(res, HTTP_BAD_REQUEST, &RFC9457Problem{ - TypeUri: TALER_URI_PROBLEM_PREFIX + "/C2EC_INVALID_PATH_PARAMETER", - Title: "invalid request path parameter", - Detail: "the withdrawal status request path parameter 'wopid' is malformed", - Instance: req.RequestURI, - }) - if err != nil { - res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR) - } - return - } - - err = DB.RegisterWithdrawal( - wpd, - registration.ReservePubKey, - ) - - if err != nil { - - err := WriteProblem(res, HTTP_INTERNAL_SERVER_ERROR, &RFC9457Problem{ - TypeUri: TALER_URI_PROBLEM_PREFIX + "/C2EC_WITHDRAW_REGISTRATION_DB_FAILURE", - Title: "database failure", - Detail: "the registration of the withdrawal failed due to db failure (error:" + err.Error() + ")", - Instance: req.RequestURI, - }) - if err != nil { - res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR) - } - return - } - - writeWithdrawalOrError(wpd, res, req.RequestURI) -} - // Get status of withdrawal associated with the given WOPID // // Parameters: @@ -236,67 +177,6 @@ func handleWithdrawalStatus(res http.ResponseWriter, req *http.Request) { writeWithdrawalOrError(wpd, res, req.RequestURI) } -func handlePaymentNotification(res http.ResponseWriter, req *http.Request) { - - wopid := req.PathValue(WOPID_PARAMETER) - wpd, err := ParseWopid(wopid) - if err != nil { - LogWarn("bank-integration-api", "wopid "+wopid+" not valid") - if wopid == "" { - err := WriteProblem(res, HTTP_BAD_REQUEST, &RFC9457Problem{ - TypeUri: TALER_URI_PROBLEM_PREFIX + "/C2EC_INVALID_PATH_PARAMETER", - Title: "invalid request path parameter", - Detail: "the withdrawal status request path parameter 'wopid' is malformed", - Instance: req.RequestURI, - }) - if err != nil { - res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR) - } - return - } - } - - jsonCodec := NewJsonCodec[C2ECPaymentNotification]() - paymentNotification, err := ReadStructFromBody(req, jsonCodec) - if err != nil { - LogWarn("bank-integration-api", fmt.Sprintf("invalid body for payment notification error=%s", err.Error())) - err := WriteProblem(res, HTTP_BAD_REQUEST, &RFC9457Problem{ - TypeUri: TALER_URI_PROBLEM_PREFIX + "/C2EC_WITHDRAW_REGISTRATION_INVALID_REQ", - Title: "invalid request", - Detail: "the payment notification request for the withdrawal is malformed (error: " + err.Error() + ")", - Instance: req.RequestURI, - }) - if err != nil { - res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR) - } - return - } - - LogInfo("bank-integration-api", "received payment notification") - - err = DB.NotifyPayment( - wpd, - paymentNotification.ProviderTransactionId, - paymentNotification.TerminalId, - paymentNotification.Amount, - paymentNotification.Fees, - ) - if err != nil { - err := WriteProblem(res, HTTP_BAD_REQUEST, &RFC9457Problem{ - TypeUri: TALER_URI_PROBLEM_PREFIX + "/C2EC_PAYMENT_NOTIFICATION_FAILED", - Title: "payment notification failed", - Detail: "the payment notification failed during the processing of the message: " + err.Error(), - Instance: req.RequestURI, - }) - if err != nil { - res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR) - } - return - } - - res.WriteHeader(HTTP_NO_CONTENT) -} - func handleWithdrawalAbort(res http.ResponseWriter, req *http.Request) { res.WriteHeader(HTTP_OK) @@ -327,7 +207,7 @@ func writeWithdrawalOrError(wopid []byte, res http.ResponseWriter, reqUri string err := WriteProblem(res, HTTP_NOT_FOUND, &RFC9457Problem{ TypeUri: TALER_URI_PROBLEM_PREFIX + "/C2EC_WITHDRAWAL_NOT_FOUND", Title: "Not Found", - Detail: "No withdrawal with wopid=" + talerBase32Encode(wopid) + " could been found.", + Detail: "No withdrawal with wopid=" + talerBinaryEncode(wopid) + " could been found.", Instance: reqUri, }) if err != nil { diff --git a/c2ec/encoding.go b/c2ec/encoding.go @@ -1,43 +1,30 @@ package main import ( - "encoding/base32" "errors" - "net/url" + "math" "strings" ) -// 32 characters for decoding, using RFC 3548. -const TALER_BASE32_CHARACTER_SET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567" +func talerBinaryEncode(byts []byte) string { -func talerBase32Encode(byts []byte) string { - return talerBase32Encoding().EncodeToString(byts) + return encodeCrock(byts) + //return talerBase32Encoding().EncodeToString(byts) } -func talerBase32Decode(str string) ([]byte, error) { +func talerBinaryDecode(str string) ([]byte, error) { - decoded, err := talerBase32Encoding().DecodeString(strings.ToUpper(str)) - if err != nil { - return nil, err - } - return decoded, nil -} - -func talerBase32Encoding() *base32.Encoding { - // 32 characters for decoding, using RFC 3548. - // character set copied from [TALER-EXCHANGE]/src/util/crypto_confirmation.c - return base32.NewEncoding(TALER_BASE32_CHARACTER_SET) + return decodeCrock(str) + // decoded, err := talerBase32Encoding().DecodeString(strings.ToUpper(str)) + // if err != nil { + // return nil, err + // } + // return decoded, nil } func ParseWopid(wopid string) ([]byte, error) { - unescaped, err := url.PathUnescape(wopid) - if err != nil { - LogError("encoding", err) - return nil, errors.New("decoding failed") - } - - wopidBytes, err := talerBase32Decode(unescaped) + wopidBytes, err := talerBinaryDecode(wopid) if err != nil { return nil, err } @@ -53,15 +40,105 @@ func ParseWopid(wopid string) ([]byte, error) { func FormatWopid(wopid []byte) string { - return url.PathEscape(talerBase32Encode(wopid)) + return talerBinaryEncode(wopid) } func ParseEddsaPubKey(key EddsaPublicKey) ([]byte, error) { - return talerBase32Decode(string(key)) + return talerBinaryDecode(string(key)) } func FormatEddsaPubKey(key []byte) EddsaPublicKey { - return EddsaPublicKey(talerBase32Encode(key)) + return EddsaPublicKey(talerBinaryEncode(key)) +} + +func decodeCrock(e string) ([]byte, error) { + size := len(e) + bitpos := 0 + bitbuf := 0 + readPosition := 0 + outLen := int(math.Floor((float64(size) * 5.0) / 8.0)) + out := make([]byte, outLen) + outPos := 0 + + getValue := func(c byte) (int, error) { + alphabet := "0123456789ABCDEFGHJKMNPQRSTVWXYZ" + switch c { + case 'o', 'O': + return 0, nil + case 'i', 'I', 'l', 'L': + return 1, nil + case 'u', 'U': + return 27, nil + } + + i := strings.IndexRune(alphabet, rune(c)) + if i > -1 && i < 32 { + return i, nil + } + + return -1, errors.New("encoding error") + } + + for readPosition < size || bitpos > 0 { + if readPosition < size { + v, err := getValue(e[readPosition]) + if err != nil { + return nil, err + } + readPosition++ + bitbuf = bitbuf<<5 | v + bitpos += 5 + } + for bitpos >= 8 { + d := byte(bitbuf >> (bitpos - 8) & 0xff) + out[outPos] = d + outPos++ + bitpos -= 8 + } + if readPosition == size && bitpos > 0 { + bitbuf = bitbuf << (8 - bitpos) & 0xff + if bitbuf == 0 { + bitpos = 0 + } else { + bitpos = 8 + } + } + } + return out, nil +} + +func encodeCrock(data []byte) string { + out := "" + bitbuf := 0 + bitpos := 0 + + encodeValue := func(value int) byte { + alphabet := "ABCDEFGHJKMNPQRSTVWXYZ" + switch { + case value >= 0 && value <= 9: + return byte('0' + value) + case value >= 10 && value <= 31: + return alphabet[value-10] + default: + panic("Invalid value for encoding") + } + } + + for _, b := range data { + bitbuf = bitbuf<<8 | int(b&0xff) + bitpos += 8 + for bitpos >= 5 { + value := bitbuf >> (bitpos - 5) & 0x1f + out += string(encodeValue(value)) + bitpos -= 5 + } + } + if bitpos > 0 { + bitbuf = bitbuf << (5 - bitpos) + value := bitbuf & 0x1f + out += string(encodeValue(value)) + } + return out } diff --git a/c2ec/encoding_test.go b/c2ec/encoding_test.go @@ -41,9 +41,9 @@ func TestTalerBase32(t *testing.T) { input := []byte("This is some text") t.Log("in:", string(input)) t.Log("in:", input) - encoded := talerBase32Encode(input) + encoded := talerBinaryEncode(input) t.Log("encoded:", encoded) - out, err := talerBase32Decode(encoded) + out, err := talerBinaryDecode(encoded) if err != nil { t.Error(err) t.FailNow() @@ -75,9 +75,9 @@ func TestTalerBase32Rand32(t *testing.T) { } t.Log("in:", input) - encoded := talerBase32Encode(input) + encoded := talerBinaryEncode(input) t.Log("encoded:", encoded) - out, err := talerBase32Decode(encoded) + out, err := talerBinaryDecode(encoded) if err != nil { t.Error(err) t.FailNow() @@ -109,9 +109,9 @@ func TestTalerBase32Rand64(t *testing.T) { } t.Log("in:", input) - encoded := talerBase32Encode(input) + encoded := talerBinaryEncode(input) t.Log("encoded:", encoded) - out, err := talerBase32Decode(encoded) + out, err := talerBinaryDecode(encoded) if err != nil { t.Error(err) t.FailNow() diff --git a/c2ec/install/installation_notes.md b/c2ec/install/installation_notes.md @@ -0,0 +1,33 @@ +# Installation of C2EC and surrounding components + +how to install exchange and C2EC + +## Prerequisites + +- Debian +- git +- postgres >= 15.6 +- it's a good idea to read [Exchange Operator Manual](https://docs.taler.net/taler-exchange-manual.html) first + +## Required Exchange binaries + +To allow the withdrawal of Taler, I will need following binaries: + +- taler-exchange-httpd +- taler-exchange-secmod-rsa +- taler-exchange-secmod-cs +- taler-exchange-secmod-eddsa +- taler-exchange-closer +- taler-exchange-wirewatch + +## Setup Commands + +`sudo echo "deb [signed-by=/etc/apt/keyrings/taler-systems.gpg] https://deb.taler.net/apt/debian bookworm main" > /etc/apt/sources.list.d/taler.list` + +`sudo wget -O /etc/apt/keyrings/taler-systems.gpg https://taler.net/taler-systems.gpg` + +`sudo apt update` + +`sudo apt install taler-exchange` + +## Configure +\ No newline at end of file diff --git a/c2ec/main.go b/c2ec/main.go @@ -15,8 +15,9 @@ import ( const GET = "GET " const POST = "POST " -const BANK_INTEGRATION_API = "/c2ec" -const WIRE_GATEWAY_API = "/wire" +// https://docs.taler.net/core/api-terminal.html#endpoints-for-integrated-sub-apis +const BANK_INTEGRATION_API = "/taler-integration" +const WIRE_GATEWAY_API = "/taler-wire-gateway" const DEFAULT_C2EC_CONFIG_PATH = "c2ec-config.yaml" // "c2ec-config.conf" @@ -94,6 +95,8 @@ func main() { setupWireGatewayRoutes(router) + setupTerminalRoutes(router) + server := http.Server{ Handler: router, } @@ -242,21 +245,11 @@ func setupBankIntegrationRoutes(router *http.ServeMux) { ) router.HandleFunc( - POST+BANK_INTEGRATION_API+WITHDRAWAL_OPERATION_BY_WOPID_PATTERN, - handleWithdrawalRegistration, - ) - - router.HandleFunc( GET+BANK_INTEGRATION_API+WITHDRAWAL_OPERATION_BY_WOPID_PATTERN, handleWithdrawalStatus, ) router.HandleFunc( - POST+BANK_INTEGRATION_API+WITHDRAWAL_OPERATION_PAYMENT_PATTERN, - handlePaymentNotification, - ) - - router.HandleFunc( POST+BANK_INTEGRATION_API+WITHDRAWAL_OPERATION_ABORTION_PATTERN, handleWithdrawalAbort, ) @@ -289,3 +282,21 @@ func setupWireGatewayRoutes(router *http.ServeMux) { adminAddIncoming, ) } + +func setupTerminalRoutes(router *http.ServeMux) { + + router.HandleFunc( + GET+TERMINAL_API_CONFIG, + handleTerminalConfig, + ) + + router.HandleFunc( + POST+TERMINAL_API_REGISTER_WITHDRAWAL, + handleWithdrawalRegistration, + ) + + router.HandleFunc( + POST+TERMINAL_API_CHECK_WITHDRAWAL, + handleWithdrawalCheck, + ) +} diff --git a/c2ec/terminals.go b/c2ec/terminals.go @@ -0,0 +1,178 @@ +package main + +import ( + "fmt" + "net/http" +) + +const TERMINAL_API_CONFIG = "/config" +const TERMINAL_API_REGISTER_WITHDRAWAL = "/withdrawals" +const TERMINAL_API_CHECK_WITHDRAWAL = "/withdrawals/:wopid/check" + +/** +* +TerminalConfig { + // Name of the API. + name: "taler-terminal"; + + // libtool-style representation of the Bank protocol version, see + // https://www.gnu.org/software/libtool/manual/html_node/Versioning.html#Versioning + // The format is "current:revision:age". + version: string; + + // Terminal provider display name to be used in user interfaces. + provider_name: string; + + // Currency supported by this terminal. + // FIXME: needed? + currency: string; + + // Wire transfer type supported by the terminal. + // FIXME: needed? + wire_type: stri +* +*/ + +type TerminalConfig struct { + Name string `json:"name"` + Version string `json:"version"` + ProviderName string `json:"provider_name"` + Currency string `json:"currency"` + WireType string `json:"wire_type"` +} + +func handleTerminalConfig(res http.ResponseWriter, req *http.Request) { + + if authenticated := AuthenticateTerminal(req); !authenticated { + res.WriteHeader(401) + return + } +} + +func handleWithdrawalRegistration(res http.ResponseWriter, req *http.Request) { + + if authenticated := AuthenticateTerminal(req); !authenticated { + res.WriteHeader(401) + return + } + + jsonCodec := NewJsonCodec[C2ECWithdrawRegistration]() + registration, err := ReadStructFromBody(req, jsonCodec) + if err != nil { + LogWarn("bank-integration-api", fmt.Sprintf("invalid body for withdrawal registration error=%s", err.Error())) + err := WriteProblem(res, HTTP_BAD_REQUEST, &RFC9457Problem{ + TypeUri: TALER_URI_PROBLEM_PREFIX + "/C2EC_WITHDRAW_REGISTRATION_INVALID_REQ", + Title: "invalid request", + Detail: "the registration request for the withdrawal is malformed (error: " + err.Error() + ")", + Instance: req.RequestURI, + }) + if err != nil { + res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR) + } + return + } + + // read and validate the wopid path parameter + wopid := req.PathValue(WOPID_PARAMETER) + wpd, err := ParseWopid(wopid) + if err != nil { + LogWarn("bank-integration-api", "wopid "+wopid+" not valid") + err := WriteProblem(res, HTTP_BAD_REQUEST, &RFC9457Problem{ + TypeUri: TALER_URI_PROBLEM_PREFIX + "/C2EC_INVALID_PATH_PARAMETER", + Title: "invalid request path parameter", + Detail: "the withdrawal status request path parameter 'wopid' is malformed", + Instance: req.RequestURI, + }) + if err != nil { + res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR) + } + return + } + + err = DB.RegisterWithdrawal( + wpd, + registration.ReservePubKey, + ) + + if err != nil { + + err := WriteProblem(res, HTTP_INTERNAL_SERVER_ERROR, &RFC9457Problem{ + TypeUri: TALER_URI_PROBLEM_PREFIX + "/C2EC_WITHDRAW_REGISTRATION_DB_FAILURE", + Title: "database failure", + Detail: "the registration of the withdrawal failed due to db failure (error:" + err.Error() + ")", + Instance: req.RequestURI, + }) + if err != nil { + res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR) + } + return + } + + writeWithdrawalOrError(wpd, res, req.RequestURI) +} + +func handleWithdrawalCheck(res http.ResponseWriter, req *http.Request) { + + if authenticated := AuthenticateTerminal(req); !authenticated { + res.WriteHeader(401) + return + } + + wopid := req.PathValue(WOPID_PARAMETER) + wpd, err := ParseWopid(wopid) + if err != nil { + LogWarn("bank-integration-api", "wopid "+wopid+" not valid") + if wopid == "" { + err := WriteProblem(res, HTTP_BAD_REQUEST, &RFC9457Problem{ + TypeUri: TALER_URI_PROBLEM_PREFIX + "/C2EC_INVALID_PATH_PARAMETER", + Title: "invalid request path parameter", + Detail: "the withdrawal status request path parameter 'wopid' is malformed", + Instance: req.RequestURI, + }) + if err != nil { + res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR) + } + return + } + } + + jsonCodec := NewJsonCodec[C2ECPaymentNotification]() + paymentNotification, err := ReadStructFromBody(req, jsonCodec) + if err != nil { + LogWarn("bank-integration-api", fmt.Sprintf("invalid body for payment notification error=%s", err.Error())) + err := WriteProblem(res, HTTP_BAD_REQUEST, &RFC9457Problem{ + TypeUri: TALER_URI_PROBLEM_PREFIX + "/C2EC_WITHDRAW_REGISTRATION_INVALID_REQ", + Title: "invalid request", + Detail: "the payment notification request for the withdrawal is malformed (error: " + err.Error() + ")", + Instance: req.RequestURI, + }) + if err != nil { + res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR) + } + return + } + + LogInfo("bank-integration-api", "received payment notification") + + err = DB.NotifyPayment( + wpd, + paymentNotification.ProviderTransactionId, + paymentNotification.TerminalId, + paymentNotification.Amount, + paymentNotification.Fees, + ) + if err != nil { + err := WriteProblem(res, HTTP_BAD_REQUEST, &RFC9457Problem{ + TypeUri: TALER_URI_PROBLEM_PREFIX + "/C2EC_PAYMENT_NOTIFICATION_FAILED", + Title: "payment notification failed", + Detail: "the payment notification failed during the processing of the message: " + err.Error(), + Instance: req.RequestURI, + }) + if err != nil { + res.WriteHeader(HTTP_INTERNAL_SERVER_ERROR) + } + return + } + + res.WriteHeader(HTTP_NO_CONTENT) +} diff --git a/c2ec/wire-gateway.go b/c2ec/wire-gateway.go @@ -189,7 +189,7 @@ func transfer(res http.ResponseWriter, req *http.Request) { return } - decodedRequestUid, err := talerBase32Decode(string(transfer.RequestUid)) + decodedRequestUid, err := talerBinaryDecode(string(transfer.RequestUid)) if err != nil { err := WriteProblem(res, HTTP_BAD_REQUEST, &RFC9457Problem{ TypeUri: TALER_URI_PROBLEM_PREFIX + "/C2EC_TRANSFER_INVALID_REQ", diff --git a/simulation/c2ec-simulation b/simulation/c2ec-simulation Binary files differ. diff --git a/simulation/encoding.go b/simulation/encoding.go @@ -1,51 +1,36 @@ package main import ( - "encoding/base32" "errors" - "fmt" - "net/url" + "math" "strings" ) -// 32 characters for decoding, using RFC 3548. -const TALER_BASE32_CHARACTER_SET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567" +func talerBinaryEncode(byts []byte) string { -func talerBase32Encode(byts []byte) string { - return talerBase32Encoding().EncodeToString(byts) + return encodeCrock(byts) + //return talerBase32Encoding().EncodeToString(byts) } -func talerBase32Decode(str string) ([]byte, error) { +func talerBinaryDecode(str string) ([]byte, error) { - decoded, err := talerBase32Encoding().DecodeString(strings.ToUpper(str)) - if err != nil { - return nil, err - } - return decoded, nil -} - -func talerBase32Encoding() *base32.Encoding { - // 32 characters for decoding, using RFC 3548. - // character set copied from [TALER-EXCHANGE]/src/util/crypto_confirmation.c - return base32.NewEncoding(TALER_BASE32_CHARACTER_SET) + return decodeCrock(str) + // decoded, err := talerBase32Encoding().DecodeString(strings.ToUpper(str)) + // if err != nil { + // return nil, err + // } + // return decoded, nil } func ParseWopid(wopid string) ([]byte, error) { - unescaped, err := url.PathUnescape(wopid) - if err != nil { - fmt.Println("encoding", err) - return nil, errors.New("decoding failed") - } - - wopidBytes, err := talerBase32Decode(unescaped) + wopidBytes, err := talerBinaryDecode(wopid) if err != nil { return nil, err } if len(wopidBytes) != 32 { err = errors.New("invalid wopid") - fmt.Println("encoding", err) return nil, err } @@ -54,10 +39,105 @@ func ParseWopid(wopid string) ([]byte, error) { func FormatWopid(wopid []byte) string { - return url.PathEscape(talerBase32Encode(wopid)) + return talerBinaryEncode(wopid) } func ParseEddsaPubKey(key EddsaPublicKey) ([]byte, error) { - return talerBase32Decode(string(key)) + return talerBinaryDecode(string(key)) +} + +func FormatEddsaPubKey(key []byte) EddsaPublicKey { + + return EddsaPublicKey(talerBinaryEncode(key)) +} + +func decodeCrock(e string) ([]byte, error) { + size := len(e) + bitpos := 0 + bitbuf := 0 + readPosition := 0 + outLen := int(math.Floor((float64(size) * 5.0) / 8.0)) + out := make([]byte, outLen) + outPos := 0 + + getValue := func(c byte) (int, error) { + alphabet := "0123456789ABCDEFGHJKMNPQRSTVWXYZ" + switch c { + case 'o', 'O': + return 0, nil + case 'i', 'I', 'l', 'L': + return 1, nil + case 'u', 'U': + return 27, nil + } + + i := strings.IndexRune(alphabet, rune(c)) + if i > -1 && i < 32 { + return i, nil + } + + return -1, errors.New("encoding error") + } + + for readPosition < size || bitpos > 0 { + if readPosition < size { + v, err := getValue(e[readPosition]) + if err != nil { + return nil, err + } + readPosition++ + bitbuf = bitbuf<<5 | v + bitpos += 5 + } + for bitpos >= 8 { + d := byte(bitbuf >> (bitpos - 8) & 0xff) + out[outPos] = d + outPos++ + bitpos -= 8 + } + if readPosition == size && bitpos > 0 { + bitbuf = bitbuf << (8 - bitpos) & 0xff + if bitbuf == 0 { + bitpos = 0 + } else { + bitpos = 8 + } + } + } + return out, nil +} + +func encodeCrock(data []byte) string { + out := "" + bitbuf := 0 + bitpos := 0 + + encodeValue := func(value int) byte { + alphabet := "ABCDEFGHJKMNPQRSTVWXYZ" + switch { + case value >= 0 && value <= 9: + return byte('0' + value) + case value >= 10 && value <= 31: + return alphabet[value-10] + default: + panic("Invalid value for encoding") + } + } + + for _, b := range data { + bitbuf = bitbuf<<8 | int(b&0xff) + bitpos += 8 + for bitpos >= 5 { + value := bitbuf >> (bitpos - 5) & 0x1f + out += string(encodeValue(value)) + bitpos -= 5 + } + } + if bitpos > 0 { + bitbuf = bitbuf << (5 - bitpos) + value := bitbuf & 0x1f + out += string(encodeValue(value)) + } + return out } diff --git a/simulation/sim-wallet.go b/simulation/sim-wallet.go @@ -128,5 +128,5 @@ func simulateReservePublicKey() string { if err != nil { return "" } - return talerBase32Encode(mockedPubKey) + return talerBinaryEncode(mockedPubKey) } diff --git a/wallee-c2ec/app/src/main/AndroidManifest.xml b/wallee-c2ec/app/src/main/AndroidManifest.xml @@ -2,6 +2,8 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> + <uses-permission android:name="android.permission.INTERNET" /> + <application android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/encoding/CryptoUtils.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/encoding/CryptoUtils.kt @@ -0,0 +1,105 @@ +/* + * This file is part of GNU Taler + * (C) 2022 Taler Systems S.A. + * + * GNU Taler is free software; you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3, or (at your option) any later version. + * + * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +/* + * The code in this file was copied from the Taler Wallet App + * source: https://git.taler.net/taler-android.git/taler-kotlin-android/src/main/java/net/taler/common/CyptoUtils.kt + */ + +package ch.bfh.habej2.wallee_c2ec.client.taler.encoding + +import kotlin.math.floor + +object CyptoUtils { + internal fun getValue(c: Char): Int { + val a = when (c) { + 'o','O' -> '0' + 'i','I','l','L' -> '1' + 'u','U' -> 'V' + else -> c + } + if (a in '0'..'9') { + return a - '0' + } + val A = if (a in 'a'..'z') a.uppercaseChar() else a + var dec = 0 + if (A in 'A'..'Z') { + if ('I' < A) dec++ + if ('L' < A) dec++ + if ('O' < A) dec++ + if ('U' < A) dec++ + return A - 'A' + 10 - dec + } + throw Error("encoding error") + } + + fun decodeCrock(e: String): ByteArray { + val size = e.length + var bitpos = 0 + var bitbuf = 0 + var readPosition = 0 + val outLen = floor((size * 5f) / 8).toInt() + val out = ByteArray(outLen) + var outPos = 0 + while (readPosition < size || bitpos > 0) { + if (readPosition < size) { + val v = getValue(e[readPosition++]) + bitbuf = bitbuf.shl(5).or(v) + bitpos += 5 + } + while (bitpos >= 8) { + val d = bitbuf.shr(bitpos -8).and(0xff).toByte() + out[outPos++] = d + bitpos -= 8 + } + if (readPosition == size && bitpos > 0) { + bitbuf = bitbuf.shl( 8 - bitpos).and(0xff) + bitpos = if (bitbuf == 0) 0 else 8 + } + } + return out + } + + fun encodeCrock(data: ByteArray): String { + val out = StringBuilder() + var bitbuf = 0 + var bitpos = 0 + for (byte in data) { + bitbuf = bitbuf.shl(8).or(byte.toInt() and 0xff) + bitpos += 8 + while (bitpos >= 5) { + val value = bitbuf.shr(bitpos - 5).and(0x1f) + out.append(encodeValue(value)) + bitpos -= 5 + } + } + if (bitpos > 0) { + bitbuf = bitbuf.shl(5 - bitpos) + val value = bitbuf.and(0x1f) + out.append(encodeValue(value)) + } + return out.toString() + } + + private fun encodeValue(value: Int): Char { + val alphabet = "ABCDEFGHJKMNPQRSTVWXYZ" + return when (value) { + in 0..9 -> ('0'.code + value).toChar() // '0' to '9' + in 10..31 -> alphabet[value - 10] // 'A' to 'Z' (without 'i' 'l' 'o' 'u') + else -> throw IllegalArgumentException("Invalid value for encoding: $value") + } + } +} +\ No newline at end of file diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/encoding/TalerBase32Codec.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/taler/encoding/TalerBase32Codec.kt @@ -1,8 +1,16 @@ package ch.bfh.habej2.wallee_c2ec.client.taler.encoding -import android.util.Base64 -import org.apache.commons.codec.binary.Base32 +import ch.bfh.habej2.wallee_c2ec.client.taler.encoding.CyptoUtils.decodeCrock +import ch.bfh.habej2.wallee_c2ec.client.taler.encoding.CyptoUtils.encodeCrock + +fun Base32Encode(byts: ByteArray): String = encodeCrock(byts) + +fun Base32Decode(enc: String) = decodeCrock(enc) + + + + + + -fun Base32Encode(byts: ByteArray): String = Base64.encodeToString(byts, 0) // Base32().encodeAsString(byts) -fun Base32Decode(enc: String) = Base32().decode(enc) -\ No newline at end of file diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/wallee/WalleeResponseHandler.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/client/wallee/WalleeResponseHandler.kt @@ -1,9 +1,10 @@ package ch.bfh.habej2.wallee_c2ec.client.wallee +import ch.bfh.habej2.wallee_c2ec.withdrawal.WithdrawalActivity import com.wallee.android.till.sdk.ResponseHandler import com.wallee.android.till.sdk.data.TransactionResponse -class WalleeResponseHandler : ResponseHandler() { +class WalleeResponseHandler(private val activity: WithdrawalActivity) : ResponseHandler() { override fun authorizeTransactionReply(response: TransactionResponse?) { @@ -16,4 +17,15 @@ class WalleeResponseHandler : ResponseHandler() { response.transaction.metaData.get("id") } + + override fun checkApiServiceCompatibilityReply( + isCompatible: Boolean?, + apiServiceVersion: String? + ) { + + if (isCompatible == null || !isCompatible) { + // just dont start withdrawals when api is not compatible + activity.finish() + } + } } \ No newline at end of file diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/AmountScreen.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/AmountScreen.kt @@ -0,0 +1,51 @@ +package ch.bfh.habej2.wallee_c2ec.withdrawal + +import android.app.Activity +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.input.KeyboardType + +@Composable +fun AmountScreen(model: WithdrawalViewModel, navigateToWhenAmountEntered: () -> Unit) { + + val activity = LocalContext.current as Activity + + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + + Text(text = "present card, trigger payment") + + TextField( + value = "", + onValueChange = { + model.updateAmount(it) + }, + label = { Text(text = "Enter amount") }, + placeholder = { Text(text = "amount") }, + keyboardOptions = KeyboardOptions( + autoCorrect = false, + keyboardType = KeyboardType.Number + ) + ) + + Button(onClick = { + navigateToWhenAmountEntered() + }) { + Text(text = "pay") + } + + Button(onClick = { + model.withdrawalOperationFailed() + activity.finish() + }) { + Text(text = "abort") + } + } +} diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/AuthorizePaymentScreen.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/AuthorizePaymentScreen.kt @@ -0,0 +1,57 @@ +package ch.bfh.habej2.wallee_c2ec.withdrawal + +import android.app.Activity +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.platform.LocalContext +import com.wallee.android.till.sdk.ApiClient +import com.wallee.android.till.sdk.data.LineItem +import com.wallee.android.till.sdk.data.Transaction +import com.wallee.android.till.sdk.data.TransactionProcessingBehavior +import java.math.BigDecimal +import java.util.Currency + +@Composable +fun AuthorizePaymentScreen(model: WithdrawalViewModel, client: ApiClient) { + + val uiState by model.uiState.collectAsState() + val activity = LocalContext.current as Activity + + val withdrawalAmount = LineItem + .ListBuilder( + uiState.encodedWopid, + BigDecimal("${uiState.amount.value}.${uiState.amount.frac}") + ) + .build() + + val transaction = Transaction.Builder(withdrawalAmount) + .setCurrency(Currency.getInstance(uiState.currency)) + .setInvoiceReference(uiState.encodedWopid) + .setMerchantReference(uiState.encodedWopid) + .setTransactionProcessingBehavior(TransactionProcessingBehavior.COMPLETE_IMMEDIATELY) + .build() + + try { + client.authorizeTransaction(transaction) + } catch (e: Exception) { + model.withdrawalOperationFailed() + activity.finish() + e.printStackTrace() + } + + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + + Text(text = "Transaction Executed") + + Button(onClick = { activity.finish() }) { + Text(text = "finish") + } + } +} diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/ExchangeSelectionScreen.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/ExchangeSelectionScreen.kt @@ -0,0 +1,42 @@ +package ch.bfh.habej2.wallee_c2ec.withdrawal + +import android.app.Activity +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.platform.LocalContext +import ch.bfh.habej2.wallee_c2ec.client.taler.config.TalerBankIntegrationConfig + +@Composable +fun ExchangeSelectionScreen( + model: WithdrawalViewModel, + onNavigateToWithdrawal: () -> Unit +) { + + val activity = LocalContext.current as Activity + + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + + Text(text = "Choose the exchange to withdraw from") + + // TODO let user select exchanges from config here + // config must contain display name, credentials (generated by cli) + // and the base url of the c2ec bank-integration api + + val ctx = LocalContext.current + Button(onClick = { + model.exchangeUpdated(TalerBankIntegrationConfig("","","","")) + onNavigateToWithdrawal() + }) { + Text(text = "withdraw") + } + + Button(onClick = { activity.finish() }) { + Text(text = "abort") + } + } +} +\ No newline at end of file diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/RegisterWithdrawalScreen.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/RegisterWithdrawalScreen.kt @@ -0,0 +1,44 @@ +package ch.bfh.habej2.wallee_c2ec.withdrawal + +import android.app.Activity +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.platform.LocalContext + +@Composable +fun RegisterWithdrawalScreen( + model: WithdrawalViewModel, + navigateToWhenRegistered: () -> Unit +) { + + val uiState by model.uiState.collectAsState() + val activity = (LocalContext.current as Activity) + + model.startAuthorizationWhenReadyOrAbort(navigateToWhenRegistered) { + activity.finish() + } + + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + + Text(text = "QR-Code content: ${formatTalerUri(uiState.exchangeBankIntegrationApiUrl, uiState.encodedWopid)}") + + QRCode(formatTalerUri(uiState.exchangeBankIntegrationApiUrl, uiState.encodedWopid)) + + Button(onClick = { + model.withdrawalOperationFailed() + activity.finish() + }) { + Text(text = "abort") + } + } +} + +private fun formatTalerUri(exchangeBankIntegrationApiPath: String, encodedWopid: String) = + "taler://withdraw/$exchangeBankIntegrationApiPath/$encodedWopid" +\ No newline at end of file diff --git a/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/WithdrawalActivity.kt b/wallee-c2ec/app/src/main/java/ch/bfh/habej2/wallee_c2ec/withdrawal/WithdrawalActivity.kt @@ -29,9 +29,15 @@ import java.util.Currency class WithdrawalActivity : ComponentActivity() { + private lateinit var walleeClient: ApiClient + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + walleeClient = ApiClient(WalleeResponseHandler(this)) + walleeClient.bind(this) + walleeClient.checkApiServiceCompatibility() + setContent { val model = WithdrawalViewModel() @@ -54,155 +60,34 @@ class WithdrawalActivity : ComponentActivity() { } } composable("authorizePaymentScreen") { - AuthorizePaymentScreen(model) + AuthorizePaymentScreen(model, walleeClient) } } } } -} - -@Composable -fun RegisterWithdrawalScreen( - model: WithdrawalViewModel, - navigateToWhenRegistered: () -> Unit -) { - - val uiState by model.uiState.collectAsState() - val activity = (LocalContext.current as Activity) - - model.startAuthorizationWhenReadyOrAbort(navigateToWhenRegistered) { - activity.finish() - } - - Column( - horizontalAlignment = Alignment.CenterHorizontally - ) { - - Text(text = "QR-Code content: ${formatTalerUri(uiState.encodedWopid)}") - - QRCode(formatTalerUri(uiState.encodedWopid)) - Button(onClick = { - model.withdrawalOperationFailed() - activity.finish() - }) { - Text(text = "abort") - } + override fun onStart() { + super.onStart() + walleeClient.bind(this) } -} - -@Composable -fun AmountScreen(model: WithdrawalViewModel, navigateToWhenAmountEntered: () -> Unit) { - - val activity = LocalContext.current as Activity - - Column( - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text(text = "present card, trigger payment") - - TextField( - value = "", - onValueChange = { - model.updateAmount(it) - }, - label = { Text(text = "Enter amount") }, - placeholder = { Text(text = "amount") }, - keyboardOptions = KeyboardOptions( - autoCorrect = false, - keyboardType = KeyboardType.Number - ) - ) - - Button(onClick = { - navigateToWhenAmountEntered() - }) { - Text(text = "pay") - } - - Button(onClick = { - model.withdrawalOperationFailed() - activity.finish() - }) { - Text(text = "abort") - } + override fun onResume() { + super.onResume() + walleeClient.bind(this) } -} - -@Composable -fun ExchangeSelectionScreen( - model: WithdrawalViewModel, - onNavigateToWithdrawal: () -> Unit -) { - - val activity = LocalContext.current as Activity - - Column( - horizontalAlignment = Alignment.CenterHorizontally - ) { - - Text(text = "Choose the exchange to withdraw from") - // TODO let user select exchanges from config here - // config must contain display name, credentials (generated by cli) - // and the base url of the c2ec bank-integration api - - val ctx = LocalContext.current - Button(onClick = { - model.exchangeUpdated(TalerBankIntegrationConfig("","","","")) - onNavigateToWithdrawal() - }) { - Text(text = "withdraw") - } - - Button(onClick = { activity.finish() }) { - Text(text = "abort") - } + override fun onStop() { + super.onStop() + walleeClient.unbind(this) } -} -@Composable -fun AuthorizePaymentScreen(model: WithdrawalViewModel) { - - val uiState by model.uiState.collectAsState() - val activity = LocalContext.current as Activity - val client = ApiClient(WalleeResponseHandler()) - - client.bind(activity) - - val withdrawalAmount = LineItem - .ListBuilder( - uiState.encodedWopid, - BigDecimal("${uiState.amount.value}.${uiState.amount.frac}") - ) - .build() - - val transaction = Transaction.Builder(withdrawalAmount) - .setCurrency(Currency.getInstance(uiState.currency)) - .setInvoiceReference(uiState.encodedWopid) - .setMerchantReference(uiState.encodedWopid) - .setTransactionProcessingBehavior(TransactionProcessingBehavior.COMPLETE_IMMEDIATELY) - .build() - - try { - client.authorizeTransaction(transaction) - } catch (e: Exception) { - e.printStackTrace() + override fun onDestroy() { + super.onDestroy() + walleeClient.unbind(this) } - client.unbind(activity) - - Column( - horizontalAlignment = Alignment.CenterHorizontally - ) { - - Text(text = "Transaction Executed") - - Button(onClick = { activity.finish() }) { - Text(text = "finish") - } + override fun onPause() { + super.onPause() + walleeClient.unbind(this) } } - -private fun formatTalerUri(encodedWopid: String) = "taler://withdraw/$encodedWopid" diff --git a/wallee-c2ec/app/src/test/java/ch/bfh/habej2/wallee_c2ec/client/taler/encoding/CyptoUtilsTest.kt b/wallee-c2ec/app/src/test/java/ch/bfh/habej2/wallee_c2ec/client/taler/encoding/CyptoUtilsTest.kt @@ -0,0 +1,54 @@ +package ch.bfh.habej2.wallee_c2ec.client.taler.encoding + +import ch.bfh.habej2.wallee_c2ec.client.taler.encoding.CyptoUtils.decodeCrock +import ch.bfh.habej2.wallee_c2ec.client.taler.encoding.CyptoUtils.encodeCrock +import org.junit.Test +import java.security.SecureRandom + +class CyptoUtilsTest { + + @Test + fun crockford() { + + val origin = rand32Bytes() + println("origin: $origin") + val encoded = encodeCrock(origin) + println("encoded: $encoded") + val decoded = decodeCrock(encoded) + println("decoded: $decoded") + + assert(origin.contentEquals(decoded)) + } + + @Test + fun crockford_taler_special() { + // see https://docs.taler.net/core/api-common.html#binary-data + + val origin = "BNR1DRKYZ676T5KMHJMNPV68V32W95S9Q35P081TJ5ZZTJ8M5WJG" + println("origin: $origin") + val decodedNormal = decodeCrock(origin) + val originWithReplacedOcrProblems1 = origin + .replace('1', 'L') + .replace('V', 'U') + .replace('0', 'O') + val originWithReplacedOcrProblems2 = origin + .replace('1', 'I') + .replace('V', 'U') + .replace('0', 'O') + println("encoded 1: $originWithReplacedOcrProblems1") + println("encoded 2: $originWithReplacedOcrProblems2") + val decoded1 = decodeCrock(originWithReplacedOcrProblems1) + val decoded2 = decodeCrock(originWithReplacedOcrProblems2) + + assert(decodedNormal.contentEquals(decoded1)) + assert(decodedNormal.contentEquals(decoded2)) + assert(decoded1.contentEquals(decoded2)) + } + + private fun rand32Bytes(): ByteArray { + val wopid = ByteArray(32) + val rand = SecureRandom() + rand.nextBytes(wopid) // will seed automatically + return wopid + } +} +\ No newline at end of file