bgpwtf/cccampix/pgpencryptor: implement service

TODO:
  * tests

Change-Id: I5d0506542070236a8ee879fcb54bc9518e23b5e3
diff --git a/bgpwtf/cccampix/pgpencryptor/model/BUILD.bazel b/bgpwtf/cccampix/pgpencryptor/model/BUILD.bazel
new file mode 100644
index 0000000..b8cd8f4
--- /dev/null
+++ b/bgpwtf/cccampix/pgpencryptor/model/BUILD.bazel
@@ -0,0 +1,19 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "go_default_library",
+    srcs = [
+        "model.go",
+        "pgp.go",
+        "schema.go",
+    ],
+    importpath = "code.hackerspace.pl/hscloud/bgpwtf/cccampix/pgpencryptor/model",
+    visibility = ["//visibility:public"],
+    deps = [
+        "//bgpwtf/cccampix/pgpencryptor/model/migrations:go_default_library",
+        "@com_github_golang_migrate_migrate_v4//:go_default_library",
+        "@com_github_golang_migrate_migrate_v4//database/cockroachdb:go_default_library",
+        "@com_github_jmoiron_sqlx//:go_default_library",
+        "@com_github_lib_pq//:go_default_library",
+    ],
+)
diff --git a/bgpwtf/cccampix/pgpencryptor/model/migrations/1565567797_init.down.sql b/bgpwtf/cccampix/pgpencryptor/model/migrations/1565567797_init.down.sql
new file mode 100644
index 0000000..ccfaa52
--- /dev/null
+++ b/bgpwtf/cccampix/pgpencryptor/model/migrations/1565567797_init.down.sql
@@ -0,0 +1 @@
+DROP TABLE pgp_keyrings;
diff --git a/bgpwtf/cccampix/pgpencryptor/model/migrations/1565567797_init.up.sql b/bgpwtf/cccampix/pgpencryptor/model/migrations/1565567797_init.up.sql
new file mode 100644
index 0000000..d1c6209
--- /dev/null
+++ b/bgpwtf/cccampix/pgpencryptor/model/migrations/1565567797_init.up.sql
@@ -0,0 +1,9 @@
+CREATE TABLE pgp_keys (
+       id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+       fingerprint STRING NOT NULL,
+       time_created INT NOT NULL,
+       okay BOOL NOT NULL,
+       key_data STRING,
+
+       UNIQUE(fingerprint)
+);
diff --git a/bgpwtf/cccampix/pgpencryptor/model/migrations/BUILD.bazel b/bgpwtf/cccampix/pgpencryptor/model/migrations/BUILD.bazel
new file mode 100644
index 0000000..e6c3bb0
--- /dev/null
+++ b/bgpwtf/cccampix/pgpencryptor/model/migrations/BUILD.bazel
@@ -0,0 +1,23 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+load("@io_bazel_rules_go//extras:embed_data.bzl", "go_embed_data")
+
+go_embed_data(
+    name = "migrations_data",
+    srcs = glob(["*.sql"]),
+    package = "migrations",
+    flatten = True,
+)
+
+go_library(
+    name = "go_default_library",
+    srcs = [
+        "migrations.go",
+        ":migrations_data",  # keep
+    ],
+    importpath = "code.hackerspace.pl/hscloud/bgpwtf/cccampix/pgpencryptor/model/migrations",
+    visibility = ["//bgpwtf/cccampix/pgpencryptor/model:__subpackages__"],
+    deps = [
+        "//go/mirko:go_default_library",
+        "@com_github_golang_migrate_migrate_v4//:go_default_library",
+    ],
+)
diff --git a/bgpwtf/cccampix/pgpencryptor/model/migrations/migrations.go b/bgpwtf/cccampix/pgpencryptor/model/migrations/migrations.go
new file mode 100644
index 0000000..5e72e6e
--- /dev/null
+++ b/bgpwtf/cccampix/pgpencryptor/model/migrations/migrations.go
@@ -0,0 +1,15 @@
+package migrations
+
+import (
+	"code.hackerspace.pl/hscloud/go/mirko"
+	"fmt"
+	"github.com/golang-migrate/migrate/v4"
+)
+
+func New(dburl string) (*migrate.Migrate, error) {
+	source, err := mirko.NewMigrationsFromBazel(Data)
+	if err != nil {
+		return nil, fmt.Errorf("could not create migrations: %v", err)
+	}
+	return migrate.NewWithSourceInstance("bazel", source, dburl)
+}
diff --git a/bgpwtf/cccampix/pgpencryptor/model/model.go b/bgpwtf/cccampix/pgpencryptor/model/model.go
new file mode 100644
index 0000000..4de3d4e
--- /dev/null
+++ b/bgpwtf/cccampix/pgpencryptor/model/model.go
@@ -0,0 +1,58 @@
+package model
+
+import (
+	"code.hackerspace.pl/hscloud/bgpwtf/cccampix/pgpencryptor/model/migrations"
+	"context"
+	"fmt"
+	migrate "github.com/golang-migrate/migrate/v4"
+	_ "github.com/golang-migrate/migrate/v4/database/cockroachdb"
+	"github.com/jmoiron/sqlx"
+	_ "github.com/lib/pq"
+	"strings"
+)
+
+type Model interface {
+	MigrateUp() error
+	PutKey(ctx context.Context, key *PgpKey) error
+	GetKey(ctx context.Context, keyID []byte) (*PgpKey, error)
+}
+
+type sqlModel struct {
+	db  *sqlx.DB
+	dsn string
+}
+
+type PgpKey struct {
+	Fingerprint []byte
+	KeyData     []byte
+	Okay        bool
+}
+
+func Connect(ctx context.Context, driver, dsn string) (Model, error) {
+	if dsn == "" {
+		return nil, fmt.Errorf("dsn cannot be empty")
+	}
+	db, err := sqlx.ConnectContext(ctx, driver, dsn)
+	if err != nil {
+		return nil, fmt.Errorf("could not connect to database: %v", err)
+	}
+	return &sqlModel{
+		db:  db,
+		dsn: dsn,
+	}, nil
+}
+
+func (m *sqlModel) MigrateUp() error {
+	dsn := "cockroach://" + strings.TrimPrefix(m.dsn, "postgres://")
+	mig, err := migrations.New(dsn)
+	if err != nil {
+		return err
+	}
+	err = mig.Up()
+	switch err {
+	case migrate.ErrNoChange:
+		return nil
+	default:
+		return err
+	}
+}
diff --git a/bgpwtf/cccampix/pgpencryptor/model/pgp.go b/bgpwtf/cccampix/pgpencryptor/model/pgp.go
new file mode 100644
index 0000000..3a9c19b
--- /dev/null
+++ b/bgpwtf/cccampix/pgpencryptor/model/pgp.go
@@ -0,0 +1,83 @@
+package model
+
+import (
+	"context"
+	"database/sql"
+	"encoding/hex"
+	"errors"
+	"fmt"
+	"time"
+)
+
+var ErrKeyNotFound = errors.New("key not found in cache")
+
+func (s *sqlModel) GetKey(ctx context.Context, keyID []byte) (*PgpKey, error) {
+	q := `
+		SELECT fingerprint, okay, key_data
+		FROM pgp_keys
+		WHERE fingerprint = $1
+		LIMIT 1
+	`
+	data := sqlPGPKey{}
+	err := s.db.Get(&data, q, hex.EncodeToString(keyID))
+
+	if err == sql.ErrNoRows {
+		return nil, ErrKeyNotFound
+	}
+
+	if err != nil {
+		return nil, err
+	}
+
+	fp, err := hex.DecodeString(data.Fingerprint)
+	if err != nil {
+		return nil, fmt.Errorf("data corruption: could not decode fingerprint")
+	}
+
+	kd, err := hex.DecodeString(data.KeyData)
+	if err != nil {
+		return nil, fmt.Errorf("data corruption: could not decode keydata")
+	}
+
+	key := PgpKey{
+		Fingerprint: fp,
+		KeyData:     kd,
+		Okay:        data.Okay,
+	}
+
+	return &key, err
+}
+
+func (s *sqlModel) PutKey(ctx context.Context, key *PgpKey) error {
+	q := `
+		INSERT INTO pgp_keys
+			(fingerprint, time_created, okay, key_data)
+			VALUES
+			(:fingerprint, :time_created, :okay, :key_data)
+		ON CONFLICT (fingerprint)
+		DO UPDATE SET
+			fingerprint = :fingerprint,
+			time_created = :time_created,
+			key_data = :key_data,
+			okay = :okay
+		WHERE pgp_keys.okay = FALSE
+	`
+
+	keyData := []byte{}
+	if key.KeyData != nil {
+		keyData = key.KeyData
+	}
+
+	data := &sqlPGPKey{
+		Fingerprint: hex.EncodeToString(key.Fingerprint),
+		KeyData:     hex.EncodeToString(keyData),
+		TimeCreated: time.Now().UnixNano(),
+		Okay:        key.Okay,
+	}
+
+	if _, err := s.db.NamedExecContext(ctx, q, data); err != nil {
+		return fmt.Errorf("INSERT pgp_keys: %v", err)
+	}
+
+	return nil
+}
diff --git a/bgpwtf/cccampix/pgpencryptor/model/schema.go b/bgpwtf/cccampix/pgpencryptor/model/schema.go
new file mode 100644
index 0000000..33cf839
--- /dev/null
+++ b/bgpwtf/cccampix/pgpencryptor/model/schema.go
@@ -0,0 +1,9 @@
+package model
+
+type sqlPGPKey struct {
+	ID          string `db:"id"`
+	Fingerprint string `db:"fingerprint"`
+	KeyData     string `db:"key_data"`
+	Okay        bool   `db:"okay"`
+	TimeCreated int64  `db:"time_created"`
+}