Commit bc637952 authored by Sybren A. Stüvel's avatar Sybren A. Stüvel

Initial checkin of somewhat-working stuff

parents
/bunq_credentials.yaml
/bunq_key_private.pem
OUT := bunqtest.exe
PKG := gitlab.com/dr.sybren/bunqapi
VERSION := $(shell git describe --tags --dirty --always)
PKG_LIST := $(shell go list ${PKG}/... | grep -v /vendor/)
STATIC_OUT := ${OUT}-v${VERSION}
PACKAGE_PATH := dist/${OUT}-${VERSION}
ifndef PACKAGE_PATH
# ${PACKAGE_PATH} is used in 'rm' commands, so it's important to check.
$(error PACKAGE_PATH is not set)
endif
all: binary
binary:
go build -i -v -o ${OUT} -ldflags="-X main.applicationVersion=${VERSION}" ${PKG}
install:
go install -i -v -ldflags="-X main.applicationVersion=${VERSION}" ${PKG}
version:
@echo "Package: ${PKG}"
@echo "Version: ${VERSION}"
test:
go test -short ${PKG_LIST}
vet:
@go vet ${PKG_LIST}
lint:
@for file in ${GO_FILES} ; do \
golint $$file ; \
done
run: binary
./${OUT}
clean:
@go clean -i -x
rm -f ${OUT}-v*
static: vet lint
go build -i -v -o ${STATIC_OUT} -tags netgo -ldflags="-extldflags \"-static\" -w -s -X main.applicationVersion=${VERSION}" ${PKG}
.gitlabAccessToken:
$(error gitlabAccessToken does not exist, visit Visit https://gitlab.com/profile/personal_access_tokens, create a Personal Access Token with API access then save it to the file .gitlabAccessToken)
release: .gitlabAccessToken package
rsync ${PACKAGE_PATH}* stuvelfoto@stuvel.eu:downloads/skyfill/ -va
go run release/release.go -version ${VERSION} -fileglob ${PACKAGE_PATH}\*
package:
@$(MAKE) _prepare_package
@$(MAKE) _package_linux
@$(MAKE) _package_windows
@$(MAKE) _package_darwin
@$(MAKE) _finish_package
package_linux:
@$(MAKE) _prepare_package
@$(MAKE) _package_linux
@$(MAKE) _finish_package
package_windows:
@$(MAKE) _prepare_package
@$(MAKE) _package_windows
@$(MAKE) _finish_package
package_darwin:
@$(MAKE) _prepare_package
@$(MAKE) _package_darwin
@$(MAKE) _finish_package
_package_linux:
@$(MAKE) --no-print-directory GOOS=linux MONGOOS=linux GOARCH=amd64 STATIC_OUT=${PACKAGE_PATH}/${OUT} _package_tar
_package_windows:
@$(MAKE) --no-print-directory GOOS=windows MONGOOS=windows GOARCH=amd64 STATIC_OUT=${PACKAGE_PATH}/${OUT}.exe _package_zip
_package_darwin:
@$(MAKE) --no-print-directory GOOS=darwin MONGOOS=osx GOARCH=amd64 STATIC_OUT=${PACKAGE_PATH}/${OUT} _package_zip
_prepare_package:
rm -rf ${PACKAGE_PATH}
mkdir -p ${PACKAGE_PATH}
cp -ua README.md LICENSE demo ${PACKAGE_PATH}/
_finish_package:
rm -r ${PACKAGE_PATH}
rm -f ${PACKAGE_PATH}.sha256
sha256sum ${PACKAGE_PATH}* | tee ${PACKAGE_PATH}.sha256
_package_tar: static
tar -C $(dir ${PACKAGE_PATH}) -zcf $(PWD)/${PACKAGE_PATH}-${GOOS}.tar.gz $(notdir ${PACKAGE_PATH})
rm ${STATIC_OUT}
_package_zip: static
cd $(dir ${PACKAGE_PATH}) && zip -9 -r -q $(notdir ${PACKAGE_PATH})-${GOOS}.zip $(notdir ${PACKAGE_PATH})
rm ${STATIC_OUT}
.PHONY: run server version static vet lint deploy package release
package bunqapi
import (
"io/ioutil"
"net/url"
"path/filepath"
"github.com/sirupsen/logrus"
"gopkg.in/yaml.v2"
)
type Credentials struct {
filename string
apiURL *url.URL
APIMode string `yaml:"apiMode"` // "production" or "sandbox"
APIKey string `yaml:"apiKey"`
InstallationToken string `yaml:"installationToken"`
ServerPublicKey string `yaml:"serverPublicKey"`
}
func LoadCredentials() Credentials {
filename, err := filepath.Abs(fileCredentials)
if err != nil {
log.WithFields(logrus.Fields{
"filename": fileCredentials,
logrus.ErrorKey: err,
}).Panic("unable to convert filename to absolute path")
}
logger := log.WithField("filename", filename)
credfile, err := ioutil.ReadFile(filename)
if err != nil {
logger.WithError(err).Fatal("unable to open credentials file")
}
creds := Credentials{filename: filename}
if err := yaml.Unmarshal(credfile, &creds); err != nil {
logger.WithError(err).Fatal("unable to decode YAML")
}
switch creds.APIMode {
case "sandbox":
creds.apiURL, err = url.Parse(urlSandbox)
case "production":
creds.apiURL, err = url.Parse(urlProduction)
default:
logger.WithField("apiMode", creds.APIMode).Fatal("invalid API mode, should be sandbox or production")
}
if err != nil {
logger.WithField("apiMode", creds.APIMode).Panic("unable to parse URL for this mode")
}
logger.WithField("url", creds.apiURL.String()).Debug("set API URL based on API mode")
return creds
}
func (creds Credentials) Endpoint(endpoint string) string {
endpointURL, err := creds.apiURL.Parse(endpoint)
if err != nil {
log.WithFields(logrus.Fields{
"apiURL": creds.apiURL.String(),
"endpoint": endpoint,
logrus.ErrorKey: err,
}).Panic("unable to parse API endpoint URL")
}
return endpointURL.String()
}
func (creds Credentials) Save() {
logger := log.WithField("filename", creds.filename)
bytes, err := yaml.Marshal(creds)
if err != nil {
logger.WithError(err).Fatal("unable to encode YAML")
}
if err := ioutil.WriteFile(creds.filename, bytes, 0600); err != nil {
logger.WithError(err).Fatal("unable to write YAML file")
}
}
package bunqapi
import (
"bytes"
"crypto/rsa"
"fmt"
"net/http"
"time"
"github.com/sirupsen/logrus"
)
type Client struct {
creds Credentials
privateKey *rsa.PrivateKey
publicKey *rsa.PublicKey
}
func NewClient(creds Credentials) *Client {
privateKey := ensureOwnRSAKey()
publicKey := privateKey.Public()
publicRSAKey, ok := publicKey.(*rsa.PublicKey)
if !ok {
log.WithField("keyType", fmt.Sprintf("%T", publicKey)).Fatal("expected public key of RSA type")
}
return &Client{
creds: creds,
privateKey: privateKey,
publicKey: publicRSAKey,
}
}
func (c *Client) NewRequest(method, endpoint string, body []byte) *http.Request {
url := c.creds.Endpoint(endpoint)
req, err := http.NewRequest(method, url, bytes.NewReader(body))
if err != nil {
log.WithFields(logrus.Fields{
"url": url,
logrus.ErrorKey: err,
}).Panic("unable to create HTTP request")
}
req.Header.Set(headerCacheControl, headerCacheControlNone)
req.Header.Set(headerUserAgent, headerUserAgentAPI)
req.Header.Set(headerXBunqGeolocation, headerGeolocationZero)
req.Header.Set(headerXBunqLanguage, headerLanguageEnglish)
req.Header.Set(headerXBunqRegion, headerRegionNL)
req.Header.Set(headerXBunqClientRequestID, time.Now().String())
if endpoint != "installation" {
req.Header.Set(headerXBunqClientAuthentication, c.creds.APIKey)
}
return req
}
package bunqapi
const libraryVersion = "0.0.1"
// URLs of the bunq API.
const (
urlSandbox = "https://public-api.sandbox.bunq.com/v1/"
urlProduction = "https://api.bunq.com/v1/"
)
// Filenames used by the bunq API.
const (
fileCredentials = "bunq_credentials.yaml"
fileRSAPublicKey = "bunq_key_public.pem"
fileRSAPrivateKey = "bunq_key_private.pem"
)
// HTTP headers
const (
headerCacheControl = "Cache-Control"
headerContentType = "Content-Type"
headerUserAgent = "User-Agent"
headerXBunqAttachmentDescription = "X-Bunq-Attachment-Description"
headerXBunqClientAuthentication = "X-Bunq-Client-Authentication"
headerXBunqClientRequestID = "X-Bunq-Client-Request-Id"
headerXBunqClientResponseID = "X-Bunq-Client-Response-Id"
headerXBunqClientSignature = "X-Bunq-Client-Signature"
headerXBunqGeolocation = "X-Bunq-Geolocation"
headerXBunqLanguage = "X-Bunq-Language"
headerXBunqRegion = "X-Bunq-Region"
)
// Default header values
const (
headerCacheControlNone = "no-cache"
headerGeolocationZero = "0 0 0 0 NL"
headerLanguageEnglish = "en_GB"
headerRegionNL = "nl_NL"
headerUserAgentAPI = "bunqapi-golang/" + libraryVersion
)
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
package bunqapi
func (c *Client) PostInstallation() {
}
package bunqapi
import (
"github.com/sirupsen/logrus"
)
var log = logrus.New()
func init() {
log.SetLevel(logrus.DebugLevel)
log.SetFormatter(&logrus.TextFormatter{
ForceColors: true,
})
}
func LogLevel(level logrus.Level) {
log.SetLevel(level)
}
func LogFormatter(formatter logrus.Formatter) {
log.SetFormatter(formatter)
}
// +build ignore
package main
import (
"github.com/sirupsen/logrus"
"gitlab.com/dr.sybren/bunqapi"
)
func main() {
logfmt := &logrus.TextFormatter{
ForceColors: true,
}
logrus.SetLevel(logrus.DebugLevel)
logrus.SetFormatter(logfmt)
bunqapi.LogLevel(logrus.GetLevel())
bunqapi.LogFormatter(logfmt)
creds := bunqapi.LoadCredentials()
logrus.WithField("apiMode", creds.APIMode).Info("loaded credentials")
client := bunqapi.NewClient(creds)
// client.PostInstallation()
req := client.NewRequest("POST", "user", nil)
client.SignRequest(req)
}
package bunqapi
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"io/ioutil"
"os"
)
func writePEM(filename, pemType string, pemBytes []byte) {
log := log.WithField("filename", filename)
pemdata := pem.EncodeToMemory(
&pem.Block{
Type: pemType,
Bytes: pemBytes,
},
)
log.Debug("writing PEM file")
err := ioutil.WriteFile(filename, pemdata, 0600)
if err != nil {
log.WithError(err).Fatal("unable to write file")
}
}
func readPEM(filename, pemType string) ([]byte, error) {
log := log.WithField("filename", filename)
pemdata, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
var block *pem.Block
for {
block, pemdata = pem.Decode(pemdata)
if block == nil {
log.WithField("expectedHeader", pemType).Fatal("unable to find expected block of PEM data")
}
if block.Type == pemType {
return block.Bytes, nil
}
}
}
func generateNewPrivateKey() *rsa.PrivateKey {
privkey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
log.WithError(err).Fatal("unable to generate new RSA key")
}
writePEM(fileRSAPrivateKey,
"RSA PRIVATE KEY",
x509.MarshalPKCS1PrivateKey(privkey))
return privkey
}
func ensureOwnRSAKey() *rsa.PrivateKey {
privlog := log.WithField("filename", fileRSAPrivateKey)
keyBytes, err := readPEM(fileRSAPrivateKey, "RSA PRIVATE KEY")
if err != nil {
if os.IsNotExist(err) {
privlog.WithError(err).Info("unable to load private key, going to generate new one")
generateNewPrivateKey()
return ensureOwnRSAKey()
}
privlog.WithError(err).Fatal("unable to load private RSA key file")
}
privkey, err := x509.ParsePKCS1PrivateKey(keyBytes)
if err != nil {
privlog.WithError(err).Fatal("unable to parse private RSA key")
}
privlog.WithField("keySizeBits", privkey.Size()*8).Debug("loaded private RSA key")
if err := privkey.Validate(); err != nil {
privlog.WithError(err).Fatal("RSA key could be loaded but is invalid")
}
return privkey
}
package bunqapi
import (
"crypto"
"crypto/rand"
"crypto/sha256"
"fmt"
"io"
"net/http"
"sort"
"strings"
"github.com/sirupsen/logrus"
)
// The headers to include in the signature. "X-Bunq-..." headers are always included.
var headersToSign = map[string]bool{
headerCacheControl: true,
headerUserAgent: true,
}
func sortedHeaderKeys(r *http.Request) []string {
headerKeys := []string{}
for key := range r.Header {
headerKeys = append(headerKeys, key)
}
sort.Strings(headerKeys)
return headerKeys
}
func (c *Client) SignRequest(r *http.Request) error {
hasher := sha256.New()
hash := func(value string) {
log.WithField("value", value).Debug("writing to hasher")
hasher.Write([]byte(value))
}
hash(fmt.Sprintf("%s %s\n", r.Method, r.URL.Path))
headerKeys := sortedHeaderKeys(r)
for _, key := range headerKeys {
if !headersToSign[key] && !strings.HasPrefix(strings.ToLower(key), "x-bunq-") {
continue
}
for _, value := range r.Header[key] {
hash(fmt.Sprintf("%s: %s\n", key, value))
}
}
hash("\n")
io.Copy(hasher, r.Body)
sum := hasher.Sum(nil)
signature, err := c.privateKey.Sign(rand.Reader, sum, crypto.SHA256)
if err != nil {
log.WithError(err).Fatal("unable to sign with private RSA key")
}
log.WithFields(logrus.Fields{
"shaSum": fmt.Sprintf("%x", sum),
"signature": fmt.Sprintf("%x", signature),
}).Debug("calculated signature")
return nil
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment