Merge "bgpwtf/cccampix: cronjobify ripe-sync"
diff --git a/bgpwtf/cccampix/pgpencryptor/BUILD.bazel b/bgpwtf/cccampix/pgpencryptor/BUILD.bazel
index 33af7b3..2596087 100644
--- a/bgpwtf/cccampix/pgpencryptor/BUILD.bazel
+++ b/bgpwtf/cccampix/pgpencryptor/BUILD.bazel
@@ -6,9 +6,13 @@
importpath = "code.hackerspace.pl/hscloud/bgpwtf/cccampix/pgpencryptor",
visibility = ["//visibility:private"],
deps = [
+ "//bgpwtf/cccampix/pgpencryptor/gpg:go_default_library",
+ "//bgpwtf/cccampix/pgpencryptor/hkp:go_default_library",
+ "//bgpwtf/cccampix/pgpencryptor/model:go_default_library",
"//bgpwtf/cccampix/proto:go_default_library",
"//go/mirko:go_default_library",
"@com_github_golang_glog//:go_default_library",
+ "@com_github_lib_pq//:go_default_library",
"@org_golang_google_grpc//codes:go_default_library",
"@org_golang_google_grpc//status:go_default_library",
],
diff --git a/bgpwtf/cccampix/pgpencryptor/gpg/BUILD.bazel b/bgpwtf/cccampix/pgpencryptor/gpg/BUILD.bazel
new file mode 100644
index 0000000..dbca7db
--- /dev/null
+++ b/bgpwtf/cccampix/pgpencryptor/gpg/BUILD.bazel
@@ -0,0 +1,8 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+ name = "go_default_library",
+ srcs = ["gpg.go"],
+ importpath = "code.hackerspace.pl/hscloud/bgpwtf/cccampix/pgpencryptor/gpg",
+ visibility = ["//visibility:public"],
+)
diff --git a/bgpwtf/cccampix/pgpencryptor/gpg/gpg.go b/bgpwtf/cccampix/pgpencryptor/gpg/gpg.go
new file mode 100644
index 0000000..09fcc7f
--- /dev/null
+++ b/bgpwtf/cccampix/pgpencryptor/gpg/gpg.go
@@ -0,0 +1,193 @@
+package gpg
+
+import (
+ "context"
+ "encoding/hex"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path"
+ "strings"
+ "time"
+)
+
+var ExecutionTimeLimit = 30 * time.Second
+var BinaryPath = "gpg"
+
+type Encryptor interface {
+ ReadCipherText(size int) ([]byte, error)
+ WritePlainText(chunk []byte) error
+ Finish()
+ Close()
+}
+
+type EncryptorFactory interface {
+ Get(recipient []byte, keyRing []byte) (Encryptor, error)
+}
+
+type CLIEncryptorFactory struct {
+}
+
+func (CLIEncryptorFactory) Get(recipient []byte, keyRing []byte) (Encryptor, error) {
+ return NewCLIEncryptor(recipient, keyRing)
+}
+
+type CLIEncryptor struct {
+ tempDir string
+ recipient []byte
+ cmd *exec.Cmd
+ stdout io.ReadCloser
+ stderr io.ReadCloser
+ stdin io.WriteCloser
+ initialized bool
+}
+
+func NewCLIEncryptor(recipient []byte, keyRing []byte) (*CLIEncryptor, error) {
+ temp, err := getGpgTempDir()
+ if err != nil {
+ return nil, err
+ }
+
+ keyRingTempPath := path.Join(temp, "_keyring")
+ err = ioutil.WriteFile(keyRingTempPath, keyRing, 0600)
+ if err != nil {
+ return nil, err
+ }
+
+ // TODO(lb5tr): test for command injection
+ importParams := []string{
+ "--homedir", temp,
+ "--import", keyRingTempPath,
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), ExecutionTimeLimit)
+ defer cancel()
+
+ cmd := exec.CommandContext(ctx, BinaryPath, importParams...)
+ err = cmd.Start()
+ if err != nil {
+ return nil, fmt.Errorf("failed to start import command: %v", err)
+ }
+
+ err = cmd.Wait()
+ if err != nil {
+ return nil, fmt.Errorf("workspace initialization failed: %v", err)
+ }
+
+ ok, err := isOkExitCode(cmd)
+ if !ok || err != nil {
+ return nil, fmt.Errorf("failed to initialize gpg workspace due to gpg execution failure")
+ }
+
+ // spawn background encryption process
+ // TODO(lb5tr): test for command injection
+ encryptParams := []string{
+ "--encrypt",
+ "--homedir", temp,
+ "--recipient", strings.ToUpper(hex.EncodeToString(recipient)),
+ "--trust-model", "always",
+ "--yes",
+ "--batch",
+ }
+
+ cmd = exec.Command(BinaryPath, encryptParams...)
+ stdout, stderr, stdin, err := makePipes(cmd)
+ if err != nil {
+ return nil, err
+ }
+
+ err = cmd.Start()
+ if err != nil {
+ return nil, fmt.Errorf("failed to start encryptor process: %v", err)
+ }
+
+ encryptor := CLIEncryptor{
+ tempDir: temp,
+ recipient: recipient,
+ cmd: cmd,
+ stdin: stdin,
+ stderr: stderr,
+ stdout: stdout,
+ initialized: true,
+ }
+
+ return &encryptor, nil
+}
+
+func (encryptor *CLIEncryptor) WritePlainText(chunk []byte) error {
+ if !encryptor.initialized {
+ return fmt.Errorf("encryptor is not initialized")
+ }
+
+ encryptor.stdin.Write(chunk)
+ return nil
+}
+
+func (encryptor *CLIEncryptor) ReadCipherText(size int) ([]byte, error) {
+ if !encryptor.initialized {
+ return nil, fmt.Errorf("encryptor is not initialized")
+ }
+
+ buf := make([]byte, size)
+ n, err := encryptor.stdout.Read(buf)
+
+ return buf[:n], err
+}
+
+func (encryptor *CLIEncryptor) Finish() {
+ encryptor.stdin.Close()
+}
+
+func (encryptor *CLIEncryptor) Close() {
+ encryptor.stdout.Close()
+ encryptor.stderr.Close()
+ encryptor.stdin.Close()
+
+ os.RemoveAll(encryptor.tempDir)
+}
+
+func getGpgTempDir() (string, error) {
+ temp, err := ioutil.TempDir("", "gpg-")
+ if err != nil {
+ return "", fmt.Errorf("failed to create temporary gpg workspace %v", err)
+ }
+
+ return temp, nil
+}
+
+func cleanupProcess(cmd *exec.Cmd) {
+ if !cmd.ProcessState.Exited() {
+ cmd.Process.Kill()
+ }
+}
+
+func isOkExitCode(cmd *exec.Cmd) (bool, error) {
+ exitCode := cmd.ProcessState.ExitCode()
+
+ if exitCode == -1 {
+ return false, fmt.Errorf("process is either still runing or was terminated by a signal")
+ }
+
+ return exitCode == 0, nil
+}
+
+func makePipes(cmd *exec.Cmd) (stdout io.ReadCloser, stderr io.ReadCloser, stdin io.WriteCloser, error error) {
+ stdout, err := cmd.StdoutPipe()
+ if err != nil {
+ return nil, nil, nil, fmt.Errorf("cmd.StdoutPipe() failed %v", err)
+ }
+
+ stdin, err = cmd.StdinPipe()
+ if err != nil {
+ return nil, nil, nil, fmt.Errorf("cmd.StdinPipe() failed %v", err)
+ }
+
+ stderr, err = cmd.StderrPipe()
+ if err != nil {
+ return nil, nil, nil, fmt.Errorf("cmd.StderrPipe() failed %v", err)
+ }
+
+ return stdout, stderr, stdin, nil
+}
diff --git a/bgpwtf/cccampix/pgpencryptor/hkp/BUILD.bazel b/bgpwtf/cccampix/pgpencryptor/hkp/BUILD.bazel
new file mode 100644
index 0000000..a2377da
--- /dev/null
+++ b/bgpwtf/cccampix/pgpencryptor/hkp/BUILD.bazel
@@ -0,0 +1,8 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+ name = "go_default_library",
+ srcs = ["hkp.go"],
+ importpath = "code.hackerspace.pl/hscloud/bgpwtf/cccampix/pgpencryptor/hkp",
+ visibility = ["//visibility:public"],
+)
diff --git a/bgpwtf/cccampix/pgpencryptor/hkp/hkp.go b/bgpwtf/cccampix/pgpencryptor/hkp/hkp.go
new file mode 100644
index 0000000..bb9ac08
--- /dev/null
+++ b/bgpwtf/cccampix/pgpencryptor/hkp/hkp.go
@@ -0,0 +1,133 @@
+package hkp
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "fmt"
+ "net/http"
+ "time"
+)
+
+// TODO(lb5tr): provide as flag
+var keyServers = []string{
+ "http://pool.sks-keyservers.net",
+ "http://keys.gnupg.net",
+}
+
+var (
+ PerServerTimeLimit = 5 * time.Second
+ PerServerRetryCount = 3
+)
+
+var ErrKeyNotFound = errors.New("not found on hkp servers")
+
+const startMarker string = "-----BEGIN PGP PUBLIC KEY BLOCK-----"
+const endMarker string = "-----END PGP PUBLIC KEY BLOCK-----"
+
+type Client interface {
+ GetKeyRing(ctx context.Context, keyID []byte) ([]byte, error)
+}
+
+type transport interface {
+ get(ctx context.Context, path string) ([]byte, error)
+}
+
+type httpTransport struct {
+}
+
+type HKP struct {
+ transport transport
+}
+
+func NewClient() Client {
+ client := HKP{
+ transport: httpTransport{},
+ }
+ return client
+}
+
+func (hkp HKP) GetKeyRing(ctx context.Context, keyID []byte) ([]byte, error) {
+ key := fmt.Sprintf("0x%x", keyID)
+ output := make(chan []byte)
+ errors := make(chan error)
+
+ go func() {
+ var lastError error
+ for _, server := range keyServers {
+ url := server + "/pks/lookup?op=get&search=" + key
+ for i := 0; i < PerServerRetryCount; i++ {
+ localCtx, cancel := context.WithTimeout(context.Background(), PerServerTimeLimit)
+ keyData, err := hkp.transport.get(localCtx, url)
+ cancel()
+
+ // ErrKeyNotFound is retriable. I've seen cases where upon retry
+ // server responds with key just fine
+
+ switch err {
+ case nil:
+ output <- keyData
+ return
+ case ctx.Err():
+ errors <- err
+ return
+ default:
+ lastError = err
+ }
+ }
+ }
+
+ errors <- lastError
+ }()
+
+ select {
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ case finalError := <-errors:
+ return nil, finalError
+ case result := <-output:
+ return result, nil
+ }
+}
+
+func (httpTransport) get(ctx context.Context, url string) ([]byte, error) {
+ localCtx, cancel := context.WithTimeout(ctx, PerServerTimeLimit)
+ defer cancel()
+
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return nil, fmt.Errorf("http.NewRequest(GET, %q): %v", url, err)
+ }
+
+ req = req.WithContext(localCtx)
+ client := http.DefaultClient
+ res, err := client.Do(req)
+
+ if err != nil {
+ return nil, fmt.Errorf("client.Do(%v): %v", req, err)
+ }
+
+ defer res.Body.Close()
+
+ if res.StatusCode != 200 {
+ if res.StatusCode == 404 {
+ return nil, ErrKeyNotFound
+ }
+
+ return nil, fmt.Errorf("got status code %d", res.StatusCode)
+ }
+
+ buf := bytes.NewBuffer([]byte{})
+ buf.ReadFrom(res.Body)
+ response := buf.Bytes()
+
+ start := bytes.Index(response, []byte(startMarker))
+ end := bytes.Index(response, []byte(endMarker))
+
+ if start == -1 || end == -1 {
+ return nil, fmt.Errorf("failed to read")
+ }
+
+ data := response[start : end+len(endMarker)]
+ return data, nil
+}
diff --git a/bgpwtf/cccampix/pgpencryptor/main.go b/bgpwtf/cccampix/pgpencryptor/main.go
index d8e410a..3d73e01 100644
--- a/bgpwtf/cccampix/pgpencryptor/main.go
+++ b/bgpwtf/cccampix/pgpencryptor/main.go
@@ -3,35 +3,248 @@
import (
"context"
"flag"
-
"github.com/golang/glog"
+ "github.com/lib/pq"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
+ "io"
+ "time"
+ "code.hackerspace.pl/hscloud/bgpwtf/cccampix/pgpencryptor/gpg"
+ "code.hackerspace.pl/hscloud/bgpwtf/cccampix/pgpencryptor/hkp"
+ "code.hackerspace.pl/hscloud/bgpwtf/cccampix/pgpencryptor/model"
pb "code.hackerspace.pl/hscloud/bgpwtf/cccampix/proto"
"code.hackerspace.pl/hscloud/go/mirko"
)
+var (
+ flagMaxClients int
+ flagChunkSize int
+ flagHkpMaxWaitTime time.Duration
+ flagDSN string
+)
+
type service struct {
+ hkpClient hkp.Client
+ encryptorFactory gpg.EncryptorFactory
+ clients chan (struct{})
+ model model.Model
}
func (s *service) KeyInfo(ctx context.Context, req *pb.KeyInfoRequest) (*pb.KeyInfoResponse, error) {
- return nil, status.Error(codes.Unimplemented, "not implemented yet")
+ var data []byte
+ var err error
+
+ switch req.Caching {
+ case pb.KeyInfoRequest_CACHING_AUTO:
+ _, err := s.model.GetKey(ctx, req.Fingerprint)
+ if err != nil {
+ data, err = s.hkpClient.GetKeyRing(ctx, req.Fingerprint)
+ switch err {
+ case nil:
+ break
+ case hkp.ErrKeyNotFound:
+ return nil, status.Errorf(codes.NotFound, "key not found: %v", err)
+ default:
+ return nil, status.Errorf(codes.Unavailable, "failed to get key from HKP servers: %v", err)
+ }
+ }
+ case pb.KeyInfoRequest_CACHING_FORCE_REMOTE:
+ data, err = s.hkpClient.GetKeyRing(ctx, req.Fingerprint)
+ switch err {
+ case nil:
+ break
+ case hkp.ErrKeyNotFound:
+ return nil, status.Errorf(codes.NotFound, "key not found: %v", err)
+ default:
+ return nil, status.Errorf(codes.Unavailable, "failed to get key from HKP servers: %v", err)
+ }
+ case pb.KeyInfoRequest_CACHING_FORCE_LOCAL:
+ _, err := s.model.GetKey(ctx, req.Fingerprint)
+ switch err {
+ case nil:
+ break
+ case model.ErrKeyNotFound:
+ return nil, status.Errorf(codes.NotFound, "key not found: %v", err)
+ default:
+ return nil, status.Errorf(codes.Unavailable, "failed to read key from local db: %v", err)
+ }
+ default:
+ return nil, status.Errorf(codes.InvalidArgument, "caching field value is invalid")
+ }
+
+ // successfully read fresh key from hkp, update db
+ if data != nil {
+ err := s.model.PutKey(ctx, &model.PgpKey{
+ Fingerprint: req.Fingerprint,
+ KeyData: data,
+ Okay: true,
+ })
+
+ if err != nil {
+ return nil, status.Errorf(codes.Unavailable, "failed to cache key received from HKP: %v", err)
+ }
+ }
+
+ return &pb.KeyInfoResponse{}, nil
}
func (s *service) Encrypt(stream pb.PGPEncryptor_EncryptServer) error {
- return status.Error(codes.Unimplemented, "not implemented yet")
+ select {
+ case s.clients <- struct{}{}:
+ break
+ case <-stream.Context().Done():
+ return status.Errorf(codes.ResourceExhausted, "PGPEncryptor to many sessions running, try again later")
+ }
+
+ defer func() {
+ <-s.clients
+ }()
+
+ ctx, _ := context.WithTimeout(context.Background(), flagHkpMaxWaitTime)
+ initialMessage, err := stream.Recv()
+ if err != nil {
+ return status.Errorf(codes.Canceled, "failed to read data from the client: %v", err)
+ }
+
+ key, err := s.model.GetKey(ctx, initialMessage.Fingerprint)
+ if err != nil {
+ if err != nil {
+ switch err {
+ case model.ErrKeyNotFound:
+ return status.Errorf(codes.NotFound, "recipient key not found: %v", err)
+ default:
+ return status.Errorf(codes.Unavailable, "error when getting keyring: %v", err)
+ }
+ }
+ }
+
+ if key.Okay == false {
+ return status.Errorf(codes.InvalidArgument, "cached key is invalid, call PGPEncryptor.KeyInfo with caching=FORCE_REMOTE to refresh")
+ }
+
+ senderDone := make(chan struct{})
+ errors := make(chan error)
+ enc, err := s.encryptorFactory.Get(key.Fingerprint, key.KeyData)
+ defer enc.Close()
+
+ if err != nil {
+ return status.Errorf(codes.Unavailable, "PGPEncryptor error while creating encryptor: %v", err)
+ }
+
+ err = enc.WritePlainText(initialMessage.Data)
+ if err != nil {
+ return err
+ }
+
+ // keep reading incoming messages and pass them to the encryptor
+ go func() {
+ defer enc.Finish()
+
+ for {
+ in, err := stream.Recv()
+
+ if err != nil {
+ if err == io.EOF {
+ return
+ }
+
+ errors <- status.Errorf(codes.Unavailable, "PGPEncryptor error while receiving message: %v", err)
+ return
+ }
+
+ err = enc.WritePlainText(in.Data)
+ if err != nil {
+ errors <- status.Errorf(codes.Unavailable, "PGPEncryptor error while writing data to encryptor: %v", err)
+ return
+ }
+
+ if in.Info == pb.EncryptRequest_CHUNK_LAST {
+ return
+ }
+ }
+ }()
+
+ // start sender routine
+ go func() {
+ defer close(senderDone)
+
+ for {
+ data, err := enc.ReadCipherText(flagChunkSize)
+ if err != nil && err != io.EOF {
+ errors <- status.Errorf(codes.Unavailable, "PGPEncryptor error while reading cipher stream: %v", err)
+ return
+ }
+
+ info := pb.EncryptResponse_CHUNK_INFO_MORE
+ if err == io.EOF {
+ info = pb.EncryptResponse_CHUNK_LAST
+ }
+
+ res := &pb.EncryptResponse{
+ Data: data,
+ Info: info,
+ }
+
+ err = stream.Send(res)
+ if err != nil {
+ errors <- status.Errorf(codes.Unavailable, "PGPEncryptor error while sending data to client: %v", err)
+ return
+ }
+
+ if info == pb.EncryptResponse_CHUNK_LAST {
+ return
+ }
+ }
+ }()
+
+ // sync with sender routine
+ select {
+ case <-senderDone:
+ return nil
+ case err := <-errors:
+ return err
+ }
}
func main() {
+ flag.IntVar(&flagMaxClients, "maxClients", 20, "maximum number of concurrent encryption sessions")
+ flag.IntVar(&flagChunkSize, "chunkSize", 1024*8, "maximum size of chunk sent back to client")
+ flag.DurationVar(&flagHkpMaxWaitTime, "hkpMaxWaitTime", 10*time.Second, "maximum time awaiting reply from HKP")
+ flag.StringVar(&flagDSN, "dsn", "", "PostrgreSQL connection string")
+
+ flag.DurationVar(&gpg.ExecutionTimeLimit, "gpgExecutionTimeLimit", 30*time.Second, "execution time limit for gpg commands")
+ flag.StringVar(&gpg.BinaryPath, "gpgPath", "gpg", "path to gpg binary")
+
+ flag.DurationVar(&hkp.PerServerTimeLimit, "hkpPerServerTimeLimit", 5*time.Second, "time for HKP server to reply with key")
+ flag.IntVar(&hkp.PerServerRetryCount, "hkpPerServerRetryCount", 3, "retry count per HKP server")
flag.Parse()
+
+ // Picking an existing postgres-like driver for sqlx.BindType to work
+ // See: https://github.com/jmoiron/sqlx/blob/ed7c52c43ee1e12a35efbcfea8dbae2d62a90370/bind.go#L24
+ mirko.TraceSQL(&pq.Driver{}, "pgx")
mi := mirko.New()
+ m, err := model.Connect(mi.Context(), "pgx", flagDSN)
+ if err != nil {
+ glog.Exitf("Failed to create model: %v", err)
+ }
+
+ err = m.MigrateUp()
+ if err != nil {
+ glog.Exitf("Failed to migrate up: %v", err)
+ }
+
if err := mi.Listen(); err != nil {
glog.Exitf("Listen failed: %v", err)
}
- s := &service{}
+ s := &service{
+ hkpClient: hkp.NewClient(),
+ encryptorFactory: gpg.CLIEncryptorFactory{},
+ clients: make(chan struct{}, flagMaxClients),
+ model: m,
+ }
pb.RegisterPGPEncryptorServer(mi.GRPC(), s)
if err := mi.Serve(); err != nil {
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"`
+}