Init commnit, First work build
This commit is contained in:
commit
d8f48970c2
52
.air.toml
Normal file
52
.air.toml
Normal file
@ -0,0 +1,52 @@
|
||||
root = "."
|
||||
testdata_dir = "testdata"
|
||||
tmp_dir = "tmp"
|
||||
|
||||
[build]
|
||||
args_bin = []
|
||||
bin = "./tmp/main"
|
||||
cmd = "go build -o ./tmp/main ./internal/cmd/server/"
|
||||
delay = 1000
|
||||
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
|
||||
exclude_file = []
|
||||
exclude_regex = ["_test.go"]
|
||||
exclude_unchanged = false
|
||||
follow_symlink = false
|
||||
full_bin = ""
|
||||
include_dir = []
|
||||
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||
include_file = []
|
||||
kill_delay = "0s"
|
||||
log = "build-errors.log"
|
||||
poll = false
|
||||
poll_interval = 0
|
||||
post_cmd = []
|
||||
pre_cmd = []
|
||||
rerun = false
|
||||
rerun_delay = 500
|
||||
send_interrupt = false
|
||||
stop_on_error = false
|
||||
|
||||
[color]
|
||||
app = ""
|
||||
build = "yellow"
|
||||
main = "magenta"
|
||||
runner = "green"
|
||||
watcher = "cyan"
|
||||
|
||||
[log]
|
||||
main_only = false
|
||||
silent = false
|
||||
time = false
|
||||
|
||||
[misc]
|
||||
clean_on_exit = false
|
||||
|
||||
[proxy]
|
||||
app_port = 0
|
||||
enabled = false
|
||||
proxy_port = 0
|
||||
|
||||
[screen]
|
||||
clear_on_rebuild = false
|
||||
keep_scroll = true
|
||||
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
tmp/
|
||||
.env
|
||||
config.yaml
|
||||
config.yml
|
||||
|
||||
54
design.md
Normal file
54
design.md
Normal file
@ -0,0 +1,54 @@
|
||||
## Key storage
|
||||
|
||||
```
|
||||
| fingerprint string | keyring |
|
||||
```
|
||||
|
||||
## Verify
|
||||
|
||||
Verify uri `/pks/v/{fingerprint}/{email}/{token}`
|
||||
|
||||
Verify pipe::> [!WARNING]
|
||||
- User send key to server
|
||||
- If key new in server
|
||||
- Send Message to emails in uids with token
|
||||
|
||||
TODO: Clear keys with unverify uids
|
||||
|
||||
UID Verify Table
|
||||
|
||||
```
|
||||
| fingerprint string | uid string | email string | verify_token string | verify bool |
|
||||
```
|
||||
|
||||
- uid "Full Name (comment) <email@example.com>"
|
||||
- email email@example.com
|
||||
|
||||
## Get key
|
||||
|
||||
URI `/pks/lookup?op={op}&search={search}`
|
||||
|
||||
### Sanitaze key
|
||||
|
||||
Key max-size
|
||||
|
||||
#### Sanitaze key and cache it
|
||||
|
||||
Before send key we can verify all signatures in our server
|
||||
|
||||
And send key with self-storade key only
|
||||
|
||||
It can so slow and loading our database.
|
||||
|
||||
Before sanitaze we can look on
|
||||
|
||||
PGP entity.SelfSignature.CreationTime
|
||||
|
||||
If time < cached_time
|
||||
- Use cache
|
||||
Else
|
||||
- Put verify state uids of fingerprints of signatures on getting key
|
||||
|
||||
// PGP entity.Identities[0].Signatures[0].CreationTime
|
||||
|
||||
|
||||
9
docker-compose.yaml
Normal file
9
docker-compose.yaml
Normal file
@ -0,0 +1,9 @@
|
||||
services:
|
||||
postgres_test:
|
||||
image: postgres:17-alpine
|
||||
environment:
|
||||
POSTGRES_DB: test_hagrid_db
|
||||
POSTGRES_USER: test_user
|
||||
POSTGRES_PASSWORD: test_password
|
||||
ports:
|
||||
- "5433:5432"
|
||||
26
go.mod
Normal file
26
go.mod
Normal file
@ -0,0 +1,26 @@
|
||||
module gadrid
|
||||
|
||||
go 1.24.4
|
||||
|
||||
require (
|
||||
github.com/ProtonMail/go-crypto v1.3.0
|
||||
github.com/emersion/go-openpgp-hkp v0.0.0-20231106091114-bb7c1dad7252
|
||||
github.com/jackc/pgx/v5 v5.7.5
|
||||
github.com/stretchr/testify v1.8.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/cloudflare/circl v1.6.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
golang.org/x/crypto v0.37.0 // indirect
|
||||
golang.org/x/sync v0.13.0 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
golang.org/x/text v0.24.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
49
go.sum
Normal file
49
go.sum
Normal file
@ -0,0 +1,49 @@
|
||||
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
|
||||
github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
|
||||
github.com/cloudflare/circl v1.6.0 h1:cr5JKic4HI+LkINy2lg3W2jF8sHCVTBncJr5gIIq7qk=
|
||||
github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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/emersion/go-openpgp-hkp v0.0.0-20231106091114-bb7c1dad7252 h1:SREvbqIjPJ+lRkk3RXkFhioEkTZgLM8GKYUHYYP1iTE=
|
||||
github.com/emersion/go-openpgp-hkp v0.0.0-20231106091114-bb7c1dad7252/go.mod h1:NrHtbDUqUo2n/tib6zrQ7TzUvUf8mD0GZHQck0wcLfw=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
|
||||
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
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/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
45
internal/cmd/server/main.go
Normal file
45
internal/cmd/server/main.go
Normal file
@ -0,0 +1,45 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"gadrid/internal/repo/storage/postgresql"
|
||||
storageService "gadrid/internal/service/storage/serv"
|
||||
|
||||
hkp "github.com/emersion/go-openpgp-hkp"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
const (
|
||||
listenAddress = "localhost:8181"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
dbpool, err := pgxpool.New(context.TODO(), "postgresql://test_user:test_password@localhost:5433/test_hagrid_db")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
dbRepo := postgresql.New(context.TODO(), dbpool)
|
||||
storageService := storageService.New(dbRepo)
|
||||
|
||||
handler := hkp.Handler{
|
||||
Adder: storageService,
|
||||
Lookuper: storageService,
|
||||
}
|
||||
|
||||
http.HandleFunc("/pks/lookup", func(w http.ResponseWriter, r *http.Request) {
|
||||
handler.ServeHTTP(w, r)
|
||||
})
|
||||
http.HandleFunc("/pks/add", func(w http.ResponseWriter, r *http.Request) {
|
||||
handler.ServeHTTP(w, r)
|
||||
})
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprint(w, "Index Page")
|
||||
})
|
||||
fmt.Println("Server is listening on address:", listenAddress)
|
||||
http.ListenAndServe(listenAddress, nil)
|
||||
}
|
||||
33
internal/controller/hkp/controller.go
Normal file
33
internal/controller/hkp/controller.go
Normal file
@ -0,0 +1,33 @@
|
||||
package hkp
|
||||
|
||||
import (
|
||||
"gadrid/internal/service/storage"
|
||||
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
hkp "github.com/emersion/go-openpgp-hkp"
|
||||
)
|
||||
|
||||
type HkpController struct {
|
||||
hkp.Adder
|
||||
hkp.Lookuper
|
||||
storage storage.StorageInterface
|
||||
// verify
|
||||
}
|
||||
|
||||
func New(st storage.StorageInterface) (HkpController, error) {
|
||||
return HkpController{
|
||||
storage: st,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s HkpController) Add(k openpgp.EntityList) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s HkpController) Get(r *hkp.LookupRequest) (openpgp.EntityList, error) {
|
||||
return openpgp.EntityList{}, nil
|
||||
}
|
||||
|
||||
func (s HkpController) Index(r *hkp.LookupRequest) ([]hkp.IndexKey, error) {
|
||||
return []hkp.IndexKey{}, nil
|
||||
}
|
||||
81
internal/repo/storage/interface.go
Normal file
81
internal/repo/storage/interface.go
Normal file
@ -0,0 +1,81 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
service "gadrid/internal/service/storage"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrKeyNotFound = fmt.Errorf("pgp key not found")
|
||||
)
|
||||
|
||||
type StorageRepositotyInterface interface {
|
||||
// AddKey handles the insertion or update of PGP keys and their associated UIDs.
|
||||
// It performs an UPSERT on pgp_keys and an INSERT (ON CONFLICT DO NOTHING) on pgp_uids.
|
||||
// The service layer is responsible for merging UID data before calling this method.
|
||||
//
|
||||
// Parameters:
|
||||
// ctx: Context for the operation.
|
||||
// keys: A slice of service.PGPkey objects to be added or updated.
|
||||
//
|
||||
// Returns:
|
||||
// error: An error if the operation fails, wrapped with context (e.g., database connection issues, SQL execution errors).
|
||||
AddKey(ctx context.Context, keys []*service.PGPkey) error
|
||||
|
||||
// VerifyUID updates the verification status of a specific User ID (UID) based on a token.
|
||||
// It checks the token's validity and expiry before setting 'verified' to true and clearing token fields.
|
||||
//
|
||||
// Parameters:
|
||||
// ctx: Context for the operation.
|
||||
// fingerprint: The fingerprint of the PGP key.
|
||||
// email: The email address associated with the UID to verify.
|
||||
// token: The verification token provided by the user.
|
||||
//
|
||||
// Returns:
|
||||
// error: Returns an error if:
|
||||
// - No matching UID entry is found (e.g., wrong fingerprint or email).
|
||||
// - The UID is already verified.
|
||||
// - The provided token does not match the stored token.
|
||||
// - The token has expired.
|
||||
// - Database operation fails (e.g., connection issues, SQL update errors).
|
||||
// Specific errors like "invalid verification token or link" are used for security reasons.
|
||||
VerifyUID(ctx context.Context, fingerprint, email, token string) error
|
||||
|
||||
// GetKey retrieves a PGP key and its associated UIDs by fingerprint.
|
||||
// It only includes UIDs that are marked as 'verified = true' in the database.
|
||||
//
|
||||
// Parameters:
|
||||
// ctx: Context for the operation.
|
||||
// fingerprint: The fingerprint of the PGP key to retrieve.
|
||||
//
|
||||
// Returns:
|
||||
// []*service.PGPkey: A slice containing the retrieved PGP key (or an empty slice if not found).
|
||||
// error: Returns an error if:
|
||||
// - The key is not found (errors.Is(err, ErrKeyNotFound)).
|
||||
// - Database query or scanning fails.
|
||||
GetKey(ctx context.Context, fingerprint string) ([]*service.PGPkey, error)
|
||||
|
||||
// Index searches for PGP keys based on a query string.
|
||||
// It matches against UID strings and emails using case-insensitive regular expressions.
|
||||
// Only keys with at least one verified UID matching the search will be returned.
|
||||
//
|
||||
// Parameters:
|
||||
// ctx: Context for the operation.
|
||||
// q: The query string (treated as a case-insensitive regular expression).
|
||||
//
|
||||
// Returns:
|
||||
// []*service.PGPkey: A slice of matching PGPKey objects, each containing only their verified UIDs.
|
||||
// error: Returns an error if database query or scanning fails.
|
||||
Index(ctx context.Context, q string) ([]*service.PGPkey, error)
|
||||
// CleanupStaleKeys identifies and deletes PGP keys that no longer have any associated UIDs.
|
||||
// This method is intended to be run periodically, e.g., by a cron job.
|
||||
//
|
||||
// Parameters:
|
||||
// ctx: Context for the operation.
|
||||
//
|
||||
// Returns:
|
||||
// int: The number of keys deleted.
|
||||
// error: An error if the cleanup operation fails.
|
||||
CleanupStaleKeys(ctx context.Context) (int, error)
|
||||
}
|
||||
179
internal/repo/storage/mock.go
Normal file
179
internal/repo/storage/mock.go
Normal file
@ -0,0 +1,179 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
service "gadrid/internal/service/storage" // Adjust this import path
|
||||
)
|
||||
|
||||
// InMemStorageMock is a mock implementation of PGPKeyStorage for testing purposes.
|
||||
// It stores data in memory, mimicking database operations.
|
||||
type InMemStorageMock struct {
|
||||
keys map[string]*service.PGPkey
|
||||
uids map[string]map[string]*service.PGPUid // fingerprint -> uid_string -> UID
|
||||
mu sync.RWMutex // Mutex for concurrent access safety
|
||||
}
|
||||
|
||||
// NewInMemStorageMock creates a new in-memory mock storage.
|
||||
func NewInMemStorageMock() *InMemStorageMock {
|
||||
return &InMemStorageMock{
|
||||
keys: make(map[string]*service.PGPkey),
|
||||
uids: make(map[string]map[string]*service.PGPUid),
|
||||
}
|
||||
}
|
||||
|
||||
// AddKey implements PGPKeyStorage interface for the mock.
|
||||
// See PGPKeyStorage interface for documentation.
|
||||
func (m *InMemStorageMock) AddKey(ctx context.Context, keys []*service.PGPkey) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
for _, k := range keys {
|
||||
// Simulate UPSERT for pgp_keys
|
||||
m.keys[k.Fingerprint] = &service.PGPkey{ // Deep copy
|
||||
Fingerprint: k.Fingerprint,
|
||||
Packet: k.Packet,
|
||||
Revoked: k.Revoked,
|
||||
// Uids are handled separately, so this slice will be empty here in the key itself
|
||||
}
|
||||
|
||||
// Ensure the inner map exists for UIDs
|
||||
if _, ok := m.uids[k.Fingerprint]; !ok {
|
||||
m.uids[k.Fingerprint] = make(map[string]*service.PGPUid)
|
||||
}
|
||||
|
||||
// Simulate INSERT (ON CONFLICT DO NOTHING) for pgp_uids
|
||||
for _, u := range k.Uids {
|
||||
if _, exists := m.uids[k.Fingerprint][u.UIDString]; !exists {
|
||||
m.uids[k.Fingerprint][u.UIDString] = &service.PGPUid{ // Deep copy
|
||||
UIDString: u.UIDString,
|
||||
Email: u.Email,
|
||||
Verify: u.Verify,
|
||||
Token: u.Token,
|
||||
TokenExpires: u.TokenExpires,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifyUID implements PGPKeyStorage interface for the mock.
|
||||
// See PGPKeyStorage interface for documentation.
|
||||
func (m *InMemStorageMock) VerifyUID(ctx context.Context, fingerprint, email, token string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
keyUids, ok := m.uids[fingerprint]
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid verification token or link") // Mimic no matching entry
|
||||
}
|
||||
|
||||
var targetUID *service.PGPUid
|
||||
var targetUIDString string // To be used as key in the map for update
|
||||
for uidStr, uid := range keyUids {
|
||||
if uid.Email == email {
|
||||
targetUID = uid
|
||||
targetUIDString = uidStr
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if targetUID == nil {
|
||||
return fmt.Errorf("invalid verification token or link") // Mimic no matching entry by email
|
||||
}
|
||||
|
||||
if targetUID.Verify {
|
||||
return fmt.Errorf("UID for %s is already verified", email)
|
||||
}
|
||||
if targetUID.Token == "" || targetUID.Token != token {
|
||||
return fmt.Errorf("invalid verification token or link")
|
||||
}
|
||||
if targetUID.TokenExpires.Before(time.Now()) {
|
||||
return fmt.Errorf("verification token has expired")
|
||||
}
|
||||
|
||||
// Simulate update
|
||||
targetUID.Verify = true
|
||||
targetUID.Token = "" // Simulate setting to null
|
||||
targetUID.TokenExpires = time.Time{} // Simulate setting to null
|
||||
|
||||
// Ensure the updated UID is correctly stored back in the map
|
||||
m.uids[fingerprint][targetUIDString] = targetUID
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetKey implements PGPKeyStorage interface for the mock.
|
||||
// See PGPKeyStorage interface for documentation.
|
||||
func (m *InMemStorageMock) GetKey(ctx context.Context, fingerprint string) ([]*service.PGPkey, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
pgpKey, ok := m.keys[fingerprint]
|
||||
if !ok {
|
||||
return nil, ErrKeyNotFound // Use the specific error defined in the interface package
|
||||
}
|
||||
|
||||
// Create a deep copy of the key
|
||||
resultKey := &service.PGPkey{
|
||||
Fingerprint: pgpKey.Fingerprint,
|
||||
Packet: pgpKey.Packet,
|
||||
Revoked: pgpKey.Revoked,
|
||||
Uids: []*service.PGPUid{}, // Only add verified UIDs
|
||||
}
|
||||
|
||||
// Filter UIDs by 'verified = true'
|
||||
if keyUids, ok := m.uids[fingerprint]; ok {
|
||||
for _, uid := range keyUids {
|
||||
if uid.Verify {
|
||||
// Deep copy the UID
|
||||
resultKey.Uids = append(resultKey.Uids, &service.PGPUid{
|
||||
UIDString: uid.UIDString,
|
||||
Email: uid.Email,
|
||||
Verify: uid.Verify,
|
||||
Token: uid.Token,
|
||||
TokenExpires: uid.TokenExpires,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return []*service.PGPkey{resultKey}, nil
|
||||
}
|
||||
|
||||
// Index implements PGPKeyStorage interface for the mock.
|
||||
// See PGPKeyStorage interface for documentation.
|
||||
func (m *InMemStorageMock) Index(ctx context.Context, q string) ([]*service.PGPkey, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
matchedFingerprints := make(map[string]bool)
|
||||
lowerQ := strings.ToLower(q) // Simulate case-insensitive search
|
||||
|
||||
for f, uidMap := range m.uids {
|
||||
for _, uid := range uidMap {
|
||||
// Simulate case-insensitive substring match for regex.
|
||||
// For a more accurate mock, you'd use the 'regexp' package here.
|
||||
if strings.Contains(strings.ToLower(uid.UIDString), lowerQ) ||
|
||||
strings.Contains(strings.ToLower(uid.Email), lowerQ) {
|
||||
matchedFingerprints[f] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var resultKeys []*service.PGPkey
|
||||
for f := range matchedFingerprints {
|
||||
// Reuse GetKey logic from mock (which filters for verified UIDs)
|
||||
keys, err := m.GetKey(ctx, f)
|
||||
if err == nil && len(keys) > 0 { // Check error from GetKey (e.g., ErrKeyNotFound)
|
||||
resultKeys = append(resultKeys, keys[0])
|
||||
}
|
||||
}
|
||||
|
||||
return resultKeys, nil
|
||||
}
|
||||
273
internal/repo/storage/postgresql/repository.go
Normal file
273
internal/repo/storage/postgresql/repository.go
Normal file
@ -0,0 +1,273 @@
|
||||
package postgresql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
repo "gadrid/internal/repo/storage"
|
||||
service "gadrid/internal/service/storage"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "embed"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
//go:embed structure.sql
|
||||
var db_struct string
|
||||
|
||||
type PostgresqlStorageRepo struct {
|
||||
poll *pgxpool.Pool
|
||||
}
|
||||
|
||||
// Get context, url
|
||||
// Return postgresql repository
|
||||
func New(ctx context.Context, pool *pgxpool.Pool) repo.StorageRepositotyInterface {
|
||||
_, err := pool.Exec(ctx, db_struct)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return PostgresqlStorageRepo{
|
||||
poll: pool,
|
||||
}
|
||||
}
|
||||
|
||||
func (s PostgresqlStorageRepo) AddKey(ctx context.Context, keys []*service.PGPkey) error {
|
||||
tx, err := s.poll.Begin(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error postgresql: AddKey: begin transaction: %w", err)
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
for _, k := range keys {
|
||||
fingerprint := k.Fingerprint
|
||||
// Upsert pgp_keys
|
||||
_, err = tx.Exec(ctx, `
|
||||
insert into pgp_keys(fingerprint, packet, revoked, update_time)
|
||||
values($1, $2, $3, now())
|
||||
on conflict (fingerprint) do update set
|
||||
packet = excluded.packet,
|
||||
revoked = excluded.revoked,
|
||||
update_time = now();
|
||||
`, fingerprint, k.Packet, k.Revoked)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error postgresql: AddKey: upsert key %s: %w", fingerprint, err)
|
||||
}
|
||||
|
||||
// Upsert pgp_uids
|
||||
batch := &pgx.Batch{}
|
||||
insertUIDQuery := `
|
||||
insert into pgp_uids(fingerprint, uid, email, verified, verification_token, token_expires_at)
|
||||
values($1, $2, $3, $4, $5, $6)
|
||||
on conflict (fingerprint, uid) do nothing;
|
||||
`
|
||||
for _, u := range k.Uids {
|
||||
batch.Queue(insertUIDQuery, fingerprint, u.UIDString, u.Email, u.Verify, u.Token, u.TokenExpires)
|
||||
}
|
||||
|
||||
br := tx.SendBatch(ctx, batch)
|
||||
// Consume all results from the batch to ensure completion
|
||||
for i := 0; i < len(k.Uids); i++ {
|
||||
_, err = br.Exec()
|
||||
if err != nil {
|
||||
br.Close()
|
||||
return fmt.Errorf("error postgresql: AddKey: batch insert uid %d for key %s: %w", i, fingerprint, err)
|
||||
}
|
||||
}
|
||||
br.Close()
|
||||
}
|
||||
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
func (s PostgresqlStorageRepo) VerifyUID(ctx context.Context, fingerprint, email, token string) error {
|
||||
tx, err := s.poll.Begin(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error postgresql: VerifyUID: begin transaction: %w", err)
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
// SELECT the UID to verify.
|
||||
var storedUID string // The full UID string (e.g., "Name <email>")
|
||||
var storedToken sql.NullString
|
||||
var tokenExpiresAt sql.NullTime
|
||||
var isVerified bool
|
||||
|
||||
row := tx.QueryRow(ctx, `
|
||||
select uid, verification_token, token_expires_at, verified
|
||||
from pgp_uids
|
||||
where fingerprint = $1 and email = $2;
|
||||
`, fingerprint, email)
|
||||
|
||||
if err = row.Scan(&storedUID, &storedToken, &tokenExpiresAt, &isVerified); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
// Not found
|
||||
return fmt.Errorf("error postgresql: VerifyUID: invalid verification token or link")
|
||||
}
|
||||
return fmt.Errorf("error postgresql: VerifyUID: failed to scan UID data: %w", err)
|
||||
}
|
||||
|
||||
if isVerified {
|
||||
return fmt.Errorf("error postgresql: VerifyUID: UID for %s is already verified", email)
|
||||
}
|
||||
|
||||
if storedToken.String == "" || storedToken.String != token {
|
||||
return fmt.Errorf("error postgresql: VerifyUID: invalid verification token or link")
|
||||
}
|
||||
|
||||
if tokenExpiresAt.Time.Before(time.Now()) {
|
||||
return fmt.Errorf("error postgresql: VerifyUID: verification token has expired")
|
||||
}
|
||||
|
||||
// UPDATE the UID status.
|
||||
result, err := tx.Exec(ctx, `
|
||||
update pgp_uids
|
||||
set verified = true,
|
||||
verification_token = null,
|
||||
token_expires_at = null
|
||||
where fingerprint = $1 and email = $2 and verification_token = $3;
|
||||
`, fingerprint, email, token)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error postgresql: VerifyUID: failed to update UID verification status: %w", err)
|
||||
}
|
||||
|
||||
rowsAffected := result.RowsAffected()
|
||||
if rowsAffected == 0 {
|
||||
return fmt.Errorf("error postgresql: VerifyUID: verification failed, token might be invalid or already used")
|
||||
}
|
||||
if rowsAffected > 1 {
|
||||
return fmt.Errorf("error postgresql: VerifyUID: multiple UIDs affected by verification (data integrity error!)")
|
||||
}
|
||||
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
func (s PostgresqlStorageRepo) GetKey(ctx context.Context, fngprt string) ([]*service.PGPkey, error) {
|
||||
var packet string
|
||||
var fingerprint string
|
||||
var revoked bool
|
||||
var update_time time.Time
|
||||
|
||||
row := s.poll.QueryRow(ctx, `
|
||||
select fingerprint, packet, revoked, update_time
|
||||
from pgp_keys
|
||||
where fingerprint ~* $1;
|
||||
`, fngprt)
|
||||
|
||||
if err := row.Scan(&fingerprint, &packet, &revoked, &update_time); err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, repo.ErrKeyNotFound
|
||||
}
|
||||
return nil, fmt.Errorf("error postgresql: GetKey: failed to scan key data for fingerprint %s: %w", fngprt, err)
|
||||
}
|
||||
|
||||
if !strings.Contains(fingerprint, fngprt) {
|
||||
return nil, repo.ErrKeyNotFound
|
||||
}
|
||||
|
||||
// Fetch all VERIFIED UIDs for this key.
|
||||
rows, err := s.poll.Query(ctx, `
|
||||
select uid, email, verification_token, token_expires_at, verified
|
||||
from pgp_uids
|
||||
where fingerprint = $1 and verified = true;
|
||||
`, fngprt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error postgresql: GetKey: failed to query UIDs for key %s: %w", fngprt, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var uids []*service.PGPUid
|
||||
for rows.Next() {
|
||||
|
||||
uid := &service.PGPUid{}
|
||||
var token sql.NullString
|
||||
var tokenExpiresAt sql.NullTime
|
||||
if err := rows.Scan(&uid.UIDString, &uid.Email, &token, &tokenExpiresAt, &uid.Verify); err != nil {
|
||||
return nil, fmt.Errorf("error postgresql: GetKey: failed to scan UID row for key %s: %w", fngprt, err)
|
||||
}
|
||||
|
||||
uid.Token = token.String
|
||||
uid.TokenExpires = tokenExpiresAt.Time
|
||||
|
||||
uids = append(uids, uid)
|
||||
|
||||
}
|
||||
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("error postgresql: GetKey: error after iterating UIDs for key %s: %w", fngprt, err)
|
||||
}
|
||||
|
||||
pgpKey := &service.PGPkey{
|
||||
Fingerprint: fngprt,
|
||||
UpdateTime: update_time,
|
||||
Packet: packet,
|
||||
Revoked: revoked,
|
||||
Uids: uids, // Only verified UIDs included
|
||||
}
|
||||
return []*service.PGPkey{pgpKey}, nil
|
||||
}
|
||||
|
||||
func (s PostgresqlStorageRepo) Index(ctx context.Context, q string) ([]*service.PGPkey, error) {
|
||||
// Search for matching fingerprints based on the query 'q'.
|
||||
fingerprintRows, err := s.poll.Query(ctx, `
|
||||
select distinct fingerprint
|
||||
from pgp_uids
|
||||
where (uid ~* $1 or email ~* $1 or fingerprint ~* $1) and verified = true;
|
||||
`, q)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error postgresql: Index: failed to query matching fingerprints for '%s': %w", q, err)
|
||||
}
|
||||
defer fingerprintRows.Close()
|
||||
|
||||
var matchingFingerprints []string
|
||||
for fingerprintRows.Next() {
|
||||
var f string
|
||||
if err := fingerprintRows.Scan(&f); err != nil {
|
||||
return nil, fmt.Errorf("error postgresql: Index: failed to scan fingerprint: %w", err)
|
||||
}
|
||||
matchingFingerprints = append(matchingFingerprints, f)
|
||||
}
|
||||
if err = fingerprintRows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("error postgresql: Index: error after iterating fingerprints: %w", err)
|
||||
}
|
||||
|
||||
if len(matchingFingerprints) == 0 {
|
||||
return []*service.PGPkey{}, nil
|
||||
}
|
||||
|
||||
// For each matching fingerprint, use GetKey to retrieve the full PGPKey object.
|
||||
var resultKeys []*service.PGPkey
|
||||
for _, fingerprint := range matchingFingerprints {
|
||||
keys, err := s.GetKey(ctx, fingerprint)
|
||||
if err != nil {
|
||||
if errors.Is(err, repo.ErrKeyNotFound) {
|
||||
continue
|
||||
}
|
||||
return nil, fmt.Errorf("error postgresql: Index: failed to retrieve key %s via GetKey: %w", fingerprint, err)
|
||||
}
|
||||
if len(keys) > 0 {
|
||||
resultKeys = append(resultKeys, keys[0])
|
||||
}
|
||||
}
|
||||
|
||||
return resultKeys, nil
|
||||
}
|
||||
func (s PostgresqlStorageRepo) CleanupStaleKeys(ctx context.Context) (int, error) {
|
||||
// The SQL query will delete keys from pgp_keys that have no corresponding entries in pgp_uids.
|
||||
result, err := s.poll.Exec(ctx, `
|
||||
delete from pgp_keys pk
|
||||
where not exists (
|
||||
select 1 from pgp_uids pu where pu.fingerprint = pk.fingerprint
|
||||
);
|
||||
`)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("error postgresql: CleanupStaleKeys: failed to delete stale keys: %w", err)
|
||||
}
|
||||
|
||||
rowsAffected := int(result.RowsAffected())
|
||||
return rowsAffected, nil
|
||||
}
|
||||
35
internal/repo/storage/postgresql/structure.sql
Normal file
35
internal/repo/storage/postgresql/structure.sql
Normal file
@ -0,0 +1,35 @@
|
||||
-- Table for PGP keys
|
||||
CREATE TABLE IF NOT EXISTS pgp_keys (
|
||||
fingerprint VARCHAR(64) PRIMARY KEY,
|
||||
packet TEXT NOT NULL, -- Armored public key block
|
||||
revoked BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
update_time TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_pgp_keys_fingerprint ON pgp_keys (fingerprint);
|
||||
|
||||
|
||||
-- Table for User IDs associated with PGP keys
|
||||
CREATE TABLE IF NOT EXISTS pgp_uids (
|
||||
fingerprint VARCHAR(64) NOT NULL, -- Foreign key to pgp_keys.fingerprint
|
||||
uid TEXT NOT NULL, -- Full User ID string (e.g., "Full Name (Comment) <email>")
|
||||
email TEXT NOT NULL,
|
||||
verified BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
verification_token TEXT UNIQUE, -- Token for email verification
|
||||
token_expires_at TIMESTAMP WITH TIME ZONE, -- Token expiry timestamp
|
||||
|
||||
UNIQUE (fingerprint, uid), -- Ensures unique User ID for a given key
|
||||
|
||||
CONSTRAINT fk_pgp_uids_fingerprint
|
||||
FOREIGN KEY (fingerprint)
|
||||
REFERENCES pgp_keys (fingerprint)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Indexes for efficient lookups
|
||||
CREATE INDEX IF NOT EXISTS idx_pgp_uids_email ON pgp_uids (email);
|
||||
CREATE INDEX IF NOT EXISTS idx_pgp_uids_verified ON pgp_uids (verified);
|
||||
|
||||
-- For advanced text search on 'uid' (user_id_string) using LIKE or regex, consider pg_trgm.
|
||||
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||
CREATE INDEX IF NOT EXISTS idx_pgp_uids_uid_gin ON pgp_uids USING GIN (uid gin_trgm_ops);
|
||||
73
internal/repo/storage/postgresql/test/initDB_test.go
Normal file
73
internal/repo/storage/postgresql/test/initDB_test.go
Normal file
@ -0,0 +1,73 @@
|
||||
package postgresql_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
const (
|
||||
testDBConnString = "postgresql://test_user:test_password@localhost:5433/test_hagrid_db"
|
||||
)
|
||||
|
||||
// setupTestDBPool initializes a connection pool for testing and ensures schema is created.
|
||||
func setupTestDBPool(t testing.TB) *pgxpool.Pool {
|
||||
// Allow time for Docker container to start
|
||||
time.Sleep(2 * time.Second)
|
||||
|
||||
pool, err := pgxpool.New(context.Background(), testDBConnString)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create test DB pool: %v", err)
|
||||
}
|
||||
|
||||
// Ping to ensure connection
|
||||
if err := pool.Ping(context.Background()); err != nil {
|
||||
pool.Close()
|
||||
t.Fatalf("Failed to ping test DB: %v", err)
|
||||
}
|
||||
|
||||
// Initialize schema (your dbStructureSQL from //go:embed)
|
||||
// Ensure dbStructureSQL is accessible here.
|
||||
// If it's embedded in main repository.go, you might need to make NewPostgresqlStorageRepo public
|
||||
// or re-embed it directly in the test file (less ideal).
|
||||
// Best: embed it in a separate package for common DB utils or make a separate TestInitDB function.
|
||||
_, err = pool.Exec(context.Background(), dbStructureSQL)
|
||||
if err != nil {
|
||||
pool.Close()
|
||||
t.Fatalf("Failed to initialize test DB schema: %v", err)
|
||||
}
|
||||
|
||||
// Enable pg_trgm for testing Index method if needed
|
||||
_, err = pool.Exec(context.Background(), "CREATE EXTENSION IF NOT EXISTS pg_trgm;")
|
||||
if err != nil {
|
||||
pool.Close()
|
||||
t.Fatalf("Failed to enable pg_trgm extension: %v", err)
|
||||
}
|
||||
// Also create the specific GIN indexes
|
||||
_, err = pool.Exec(context.Background(), "CREATE INDEX IF NOT EXISTS idx_pgp_uids_uid_gin ON pgp_uids USING GIN (uid gin_trgm_ops);")
|
||||
if err != nil {
|
||||
pool.Close()
|
||||
t.Fatalf("Failed to create idx_pgp_uids_uid_gin: %v", err)
|
||||
}
|
||||
_, err = pool.Exec(context.Background(), "CREATE INDEX IF NOT EXISTS idx_pgp_uids_email_gin ON pgp_uids USING GIN (email gin_trgm_ops);")
|
||||
if err != nil {
|
||||
pool.Close()
|
||||
t.Fatalf("Failed to create idx_pgp_uids_email_gin: %v", err)
|
||||
}
|
||||
|
||||
return pool
|
||||
}
|
||||
|
||||
// cleanupTestDB clears all data from relevant tables.
|
||||
func cleanupTestDB(t testing.TB, pool *pgxpool.Pool) {
|
||||
// Truncate tables, CASCADE if needed
|
||||
_, err := pool.Exec(context.Background(), `
|
||||
truncate table pgp_uids cascade;
|
||||
truncate table pgp_keys cascade;
|
||||
`)
|
||||
if err != nil {
|
||||
t.Errorf("Failed to cleanup test DB: %v", err)
|
||||
}
|
||||
}
|
||||
360
internal/repo/storage/postgresql/test/postgresql_test.go
Normal file
360
internal/repo/storage/postgresql/test/postgresql_test.go
Normal file
@ -0,0 +1,360 @@
|
||||
package postgresql_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
_ "embed"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
repository "gadrid/internal/repo/storage"
|
||||
psqlrepo "gadrid/internal/repo/storage/postgresql"
|
||||
service "gadrid/internal/service/storage"
|
||||
)
|
||||
|
||||
//go:embed structure.sql
|
||||
var dbStructureSQL string
|
||||
|
||||
func TestPostgresqlStorageRepo_AddKey(t *testing.T) {
|
||||
pool := setupTestDBPool(t)
|
||||
defer pool.Close()
|
||||
defer cleanupTestDB(t, pool)
|
||||
|
||||
repo := psqlrepo.New(context.Background(), pool)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// --- Test Case 1: Add a new key ---
|
||||
t.Run("Add new key successfully", func(t *testing.T) {
|
||||
key := &service.PGPkey{
|
||||
Fingerprint: "FINGERPRINT_NEW_KEY",
|
||||
Packet: "-----BEGIN PGP PUBLIC KEY BLOCK-----NEW_KEY_PACKET-----END PGP PUBLIC KEY BLOCK-----",
|
||||
Revoked: false,
|
||||
Uids: []*service.PGPUid{
|
||||
{UIDString: "New User <new@example.com>", Email: "new@example.com", Verify: false, Token: "token1", TokenExpires: time.Now().Add(time.Hour)},
|
||||
},
|
||||
}
|
||||
err := repo.AddKey(ctx, []*service.PGPkey{key})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify key was added
|
||||
retrievedKeys, err := repo.GetKey(ctx, key.Fingerprint)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, retrievedKeys, 1)
|
||||
assert.Equal(t, key.Fingerprint, retrievedKeys[0].Fingerprint)
|
||||
assert.Equal(t, key.Packet, retrievedKeys[0].Packet)
|
||||
assert.False(t, retrievedKeys[0].Revoked)
|
||||
assert.Empty(t, retrievedKeys[0].Uids) // GetKey only returns verified UIDs
|
||||
|
||||
// Verify UID was added (check DB directly for unverified)
|
||||
var uidCount int
|
||||
err = pool.QueryRow(ctx, "select count(*) from pgp_uids where fingerprint = $1 and uid = $2", key.Fingerprint, "New User <new@example.com>").Scan(&uidCount)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, uidCount)
|
||||
})
|
||||
|
||||
// --- Test Case 2: Update an existing key (packet, revoked status) ---
|
||||
t.Run("Update existing key packet and revoked status", func(t *testing.T) {
|
||||
initialKey := &service.PGPkey{
|
||||
Fingerprint: "FINGERPRINT_EXISTING",
|
||||
Packet: "-----BEGIN PGP PUBLIC KEY BLOCK-----INITIAL_PACKET-----END PGP PUBLIC KEY BLOCK-----",
|
||||
Revoked: false,
|
||||
Uids: []*service.PGPUid{
|
||||
{UIDString: "Existing User <existing@example.com>", Email: "existing@example.com", Verify: false, Token: "initial_token", TokenExpires: time.Now().Add(time.Hour)},
|
||||
},
|
||||
}
|
||||
err := repo.AddKey(ctx, []*service.PGPkey{initialKey})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Simulate an update: new packet, revoked true
|
||||
updatedKey := &service.PGPkey{
|
||||
Fingerprint: "FINGERPRINT_EXISTING",
|
||||
Packet: "-----BEGIN PGP PUBLIC KEY BLOCK-----UPDATED_PACKET-----END PGP PUBLIC KEY BLOCK-----",
|
||||
Revoked: true,
|
||||
Uids: []*service.PGPUid{
|
||||
{UIDString: "Existing User <existing@example.com>", Email: "existing@example.com", Verify: false, Token: "updated_token", TokenExpires: time.Now().Add(time.Hour * 2)},
|
||||
},
|
||||
}
|
||||
err = repo.AddKey(ctx, []*service.PGPkey{updatedKey})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify key was updated
|
||||
retrievedKeys, err := repo.GetKey(ctx, updatedKey.Fingerprint)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, retrievedKeys, 1)
|
||||
assert.Equal(t, updatedKey.Fingerprint, retrievedKeys[0].Fingerprint)
|
||||
assert.Equal(t, updatedKey.Packet, retrievedKeys[0].Packet)
|
||||
assert.True(t, retrievedKeys[0].Revoked)
|
||||
|
||||
// Verify UID was NOT updated (due to on conflict do nothing)
|
||||
var storedToken string
|
||||
err = pool.QueryRow(ctx, "select verification_token from pgp_uids where fingerprint = $1 and uid = $2", updatedKey.Fingerprint, "Existing User <existing@example.com>").Scan(&storedToken)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "initial_token", storedToken) // Should still be initial_token
|
||||
})
|
||||
|
||||
// --- Test Case 3: Add new UID to existing key ---
|
||||
t.Run("Add new UID to existing key", func(t *testing.T) {
|
||||
key := &service.PGPkey{
|
||||
Fingerprint: "FINGERPRINT_ADD_UID",
|
||||
Packet: "-----BEGIN PGP PUBLIC KEY BLOCK-----ADD_UID_KEY-----END PGP PUBLIC KEY BLOCK-----",
|
||||
Revoked: false,
|
||||
Uids: []*service.PGPUid{
|
||||
{UIDString: "First UID <first@example.com>", Email: "first@example.com", Verify: false, Token: "token_first", TokenExpires: time.Now().Add(time.Hour)},
|
||||
},
|
||||
}
|
||||
err := repo.AddKey(ctx, []*service.PGPkey{key})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Add a second UID to the same key
|
||||
updatedKey := &service.PGPkey{
|
||||
Fingerprint: "FINGERPRINT_ADD_UID",
|
||||
Packet: "-----BEGIN PGP PUBLIC KEY BLOCK-----ADD_UID_KEY_UPDATED-----END PGP PUBLIC KEY BLOCK-----",
|
||||
Revoked: false,
|
||||
Uids: []*service.PGPUid{
|
||||
{UIDString: "First UID <first@example.com>", Email: "first@example.com", Verify: false, Token: "token_first", TokenExpires: time.Now().Add(time.Hour)},
|
||||
{UIDString: "Second UID <second@example.com>", Email: "second@example.com", Verify: false, Token: "token_second", TokenExpires: time.Now().Add(time.Hour)},
|
||||
},
|
||||
}
|
||||
err = repo.AddKey(ctx, []*service.PGPkey{updatedKey})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify both UIDs exist
|
||||
var uidCount int
|
||||
err = pool.QueryRow(ctx, "select count(*) from pgp_uids where fingerprint = $1", updatedKey.Fingerprint).Scan(&uidCount)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 2, uidCount)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPostgresqlStorageRepo_VerifyUID(t *testing.T) {
|
||||
pool := setupTestDBPool(t)
|
||||
defer pool.Close()
|
||||
defer cleanupTestDB(t, pool)
|
||||
|
||||
repo := psqlrepo.New(context.Background(), pool)
|
||||
ctx := context.Background()
|
||||
|
||||
// Add a key with an unverified UID
|
||||
keyFingerprint := "FINGERPRINT_VERIFY_KEY"
|
||||
uidEmail := "verify@test.com"
|
||||
uidToken := "verifytoken123"
|
||||
uidExpires := time.Now().Add(time.Hour)
|
||||
|
||||
err := repo.AddKey(ctx, []*service.PGPkey{
|
||||
{
|
||||
Fingerprint: keyFingerprint,
|
||||
Packet: "...",
|
||||
Revoked: false,
|
||||
Uids: []*service.PGPUid{
|
||||
{UIDString: "Verify Me <" + uidEmail + ">", Email: uidEmail, Verify: false, Token: uidToken, TokenExpires: uidExpires},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("Successfully verify UID", func(t *testing.T) {
|
||||
err := repo.VerifyUID(ctx, keyFingerprint, uidEmail, uidToken)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify status in DB
|
||||
var verified bool
|
||||
var token sql.NullString
|
||||
var expires sql.NullTime
|
||||
err = pool.QueryRow(ctx, "select verified, verification_token, token_expires_at from pgp_uids where fingerprint = $1 and email = $2", keyFingerprint, uidEmail).Scan(&verified, &token, &expires)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, verified)
|
||||
assert.False(t, token.Valid) // Should be null
|
||||
assert.False(t, expires.Valid) // Should be null
|
||||
})
|
||||
|
||||
t.Run("Fail verification with wrong token", func(t *testing.T) {
|
||||
err := repo.VerifyUID(ctx, keyFingerprint, "wrongverify@email.io", "wrongtoken")
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "invalid verification token or link")
|
||||
|
||||
// Ensure status remains verified from previous successful run
|
||||
var verified bool
|
||||
err = pool.QueryRow(ctx, "select verified from pgp_uids where fingerprint = $1 and email = $2", keyFingerprint, uidEmail).Scan(&verified)
|
||||
require.NoError(t, err)
|
||||
assert.True(t, verified)
|
||||
})
|
||||
|
||||
t.Run("Fail verification for already verified UID", func(t *testing.T) {
|
||||
err := repo.VerifyUID(ctx, keyFingerprint, uidEmail, uidToken)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "already verified")
|
||||
})
|
||||
|
||||
t.Run("Fail verification with expired token", func(t *testing.T) {
|
||||
// Add another key with an expired token
|
||||
expiredFingerprint := "FINGERPRINT_EXPIRED_TOKEN"
|
||||
expiredEmail := "expired@test.com"
|
||||
expiredToken := "expiredtoken"
|
||||
expiredTime := time.Now().Add(-time.Hour)
|
||||
|
||||
err := repo.AddKey(ctx, []*service.PGPkey{
|
||||
{
|
||||
Fingerprint: expiredFingerprint,
|
||||
Packet: "...",
|
||||
Revoked: false,
|
||||
Uids: []*service.PGPUid{
|
||||
{UIDString: "Expired <" + expiredEmail + ">", Email: expiredEmail, Verify: false, Token: expiredToken, TokenExpires: expiredTime},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
err = repo.VerifyUID(ctx, expiredFingerprint, expiredEmail, expiredToken)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "token has expired")
|
||||
|
||||
// Ensure it's still unverified
|
||||
var verified bool
|
||||
err = pool.QueryRow(ctx, "select verified from pgp_uids where fingerprint = $1 and email = $2", expiredFingerprint, expiredEmail).Scan(&verified)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, verified)
|
||||
})
|
||||
}
|
||||
|
||||
func TestPostgresqlStorageRepo_GetKey(t *testing.T) {
|
||||
pool := setupTestDBPool(t)
|
||||
defer pool.Close()
|
||||
defer cleanupTestDB(t, pool)
|
||||
|
||||
repo := psqlrepo.New(context.Background(), pool)
|
||||
ctx := context.Background()
|
||||
|
||||
// Add a key with mixed UIDs (verified and unverified)
|
||||
testFingerprint := "FINGERPRINT_GET_KEY"
|
||||
err := repo.AddKey(ctx, []*service.PGPkey{
|
||||
{
|
||||
Fingerprint: testFingerprint,
|
||||
Packet: "PACKET_DATA",
|
||||
Revoked: false,
|
||||
Uids: []*service.PGPUid{
|
||||
{UIDString: "Verified User <verified@example.com>", Email: "verified@example.com", Verify: false, Token: "tok_v", TokenExpires: time.Now().Add(time.Hour)},
|
||||
{UIDString: "Unverified User <unverified@example.com>", Email: "unverified@example.com", Verify: false, Token: "tok_uv", TokenExpires: time.Now().Add(time.Hour)},
|
||||
{UIDString: "Another Verified <another@example.com>", Email: "another@example.com", Verify: false, Token: "tok_av", TokenExpires: time.Now().Add(time.Hour)},
|
||||
},
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Manually verify two UIDs for the test
|
||||
require.NoError(t, repo.VerifyUID(ctx, testFingerprint, "verified@example.com", "tok_v"))
|
||||
require.NoError(t, repo.VerifyUID(ctx, testFingerprint, "another@example.com", "tok_av"))
|
||||
|
||||
t.Run("Retrieve key with only verified UIDs", func(t *testing.T) {
|
||||
retrievedKeys, err := repo.GetKey(ctx, testFingerprint)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, retrievedKeys, 1)
|
||||
|
||||
key := retrievedKeys[0]
|
||||
assert.Equal(t, testFingerprint, key.Fingerprint)
|
||||
assert.Equal(t, "PACKET_DATA", key.Packet)
|
||||
assert.False(t, key.Revoked)
|
||||
assert.Len(t, key.Uids, 2) // Only 2 verified UIDs should be returned
|
||||
|
||||
// Check UIDs content
|
||||
foundEmails := make(map[string]bool)
|
||||
for _, uid := range key.Uids {
|
||||
assert.True(t, uid.Verify) // All returned UIDs must be verified
|
||||
foundEmails[uid.Email] = true
|
||||
}
|
||||
assert.True(t, foundEmails["verified@example.com"])
|
||||
assert.True(t, foundEmails["another@example.com"])
|
||||
assert.False(t, foundEmails["unverified@example.com"]) // Should not be present
|
||||
})
|
||||
|
||||
t.Run("Retrieve non-existent key", func(t *testing.T) {
|
||||
k, err := repo.GetKey(ctx, "NON_EXISTENT_FINGERPRINT")
|
||||
assert.Len(t, k, 0)
|
||||
require.Error(t, err)
|
||||
assert.True(t, errors.Is(err, repository.ErrKeyNotFound))
|
||||
})
|
||||
}
|
||||
|
||||
func TestPostgresqlStorageRepo_Index(t *testing.T) {
|
||||
pool := setupTestDBPool(t)
|
||||
defer pool.Close()
|
||||
defer cleanupTestDB(t, pool)
|
||||
|
||||
repo := psqlrepo.New(context.Background(), pool)
|
||||
ctx := context.Background()
|
||||
|
||||
// Add multiple keys with various UIDs
|
||||
keysToLoad := []*service.PGPkey{
|
||||
{
|
||||
Fingerprint: "FINGERPRINT_ALPHA", Packet: "alpha_packet", Revoked: false,
|
||||
Uids: []*service.PGPUid{
|
||||
{UIDString: "Alpha User <alpha@example.com>", Email: "alpha@example.com", Verify: false, Token: "t1", TokenExpires: time.Now().Add(time.Hour)},
|
||||
{UIDString: "Test Alpha <testalpha@mail.com>", Email: "testalpha@mail.com", Verify: false, Token: "t2", TokenExpires: time.Now().Add(time.Hour)},
|
||||
},
|
||||
},
|
||||
{
|
||||
Fingerprint: "FINGERPRINT_BETA", Packet: "beta_packet", Revoked: false,
|
||||
Uids: []*service.PGPUid{
|
||||
{UIDString: "Beta User <beta@example.com>", Email: "beta@example.com", Verify: false, Token: "t3", TokenExpires: time.Now().Add(time.Hour)},
|
||||
},
|
||||
},
|
||||
{
|
||||
Fingerprint: "FINGERPRINT_GAMMA", Packet: "gamma_packet", Revoked: false,
|
||||
Uids: []*service.PGPUid{
|
||||
{UIDString: "Gamma User <gamma@example.com>", Email: "gamma@example.com", Verify: false, Token: "t4", TokenExpires: time.Now().Add(time.Hour)},
|
||||
{UIDString: "Test Gamma <testgamma@mail.com>", Email: "testgamma@mail.com", Verify: false, Token: "t5", TokenExpires: time.Now().Add(time.Hour)},
|
||||
},
|
||||
},
|
||||
}
|
||||
require.NoError(t, repo.AddKey(ctx, keysToLoad))
|
||||
|
||||
// Manually verify some UIDs
|
||||
require.NoError(t, repo.VerifyUID(ctx, "FINGERPRINT_ALPHA", "alpha@example.com", "t1"))
|
||||
require.NoError(t, repo.VerifyUID(ctx, "FINGERPRINT_BETA", "beta@example.com", "t3"))
|
||||
require.NoError(t, repo.VerifyUID(ctx, "FINGERPRINT_GAMMA", "testgamma@mail.com", "t5")) // Verify only one UID for gamma
|
||||
|
||||
t.Run("Search by full UID string", func(t *testing.T) {
|
||||
results, err := repo.Index(ctx, "Alpha User")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, results, 1)
|
||||
assert.Equal(t, "FINGERPRINT_ALPHA", results[0].Fingerprint)
|
||||
assert.Len(t, results[0].Uids, 1) // Only 1 verified UID from alpha
|
||||
assert.Equal(t, "alpha@example.com", results[0].Uids[0].Email)
|
||||
})
|
||||
|
||||
t.Run("Search by email part", func(t *testing.T) {
|
||||
results, err := repo.Index(ctx, "mail")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, results, 1) // Only FINGERPRINT_GAMMA has a *verified* UID with @mail.com
|
||||
assert.Equal(t, "FINGERPRINT_GAMMA", results[0].Fingerprint)
|
||||
assert.Len(t, results[0].Uids, 1)
|
||||
assert.Equal(t, "testgamma@mail.com", results[0].Uids[0].Email)
|
||||
})
|
||||
|
||||
t.Run("Search by part of name (case-insensitive)", func(t *testing.T) {
|
||||
results, err := repo.Index(ctx, "beta")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, results, 1)
|
||||
assert.Equal(t, "FINGERPRINT_BETA", results[0].Fingerprint)
|
||||
assert.Len(t, results[0].Uids, 1)
|
||||
assert.Equal(t, "beta@example.com", results[0].Uids[0].Email)
|
||||
})
|
||||
|
||||
t.Run("Search for non-existent query", func(t *testing.T) {
|
||||
results, err := repo.Index(ctx, "nonexistent")
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, results)
|
||||
})
|
||||
|
||||
t.Run("Search for unverified UID (should not return key)", func(t *testing.T) {
|
||||
// FINGERPRINT_ALPHA has an unverified UID "Test Alpha <testalpha@mail.com>"
|
||||
results, err := repo.Index(ctx, "Test Alpha")
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, results)
|
||||
})
|
||||
}
|
||||
35
internal/repo/storage/postgresql/test/structure.sql
Normal file
35
internal/repo/storage/postgresql/test/structure.sql
Normal file
@ -0,0 +1,35 @@
|
||||
-- Table for PGP keys
|
||||
CREATE TABLE IF NOT EXISTS pgp_keys (
|
||||
fingerprint VARCHAR(64) PRIMARY KEY,
|
||||
packet TEXT NOT NULL, -- Armored public key block
|
||||
revoked BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
update_time TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_pgp_keys_fingerprint ON pgp_keys (fingerprint);
|
||||
|
||||
|
||||
-- Table for User IDs associated with PGP keys
|
||||
CREATE TABLE IF NOT EXISTS pgp_uids (
|
||||
fingerprint VARCHAR(64) NOT NULL, -- Foreign key to pgp_keys.fingerprint
|
||||
uid TEXT NOT NULL, -- Full User ID string (e.g., "Full Name (Comment) <email>")
|
||||
email TEXT NOT NULL,
|
||||
verified BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
verification_token TEXT UNIQUE, -- Token for email verification
|
||||
token_expires_at TIMESTAMP WITH TIME ZONE, -- Token expiry timestamp
|
||||
|
||||
UNIQUE (fingerprint, uid), -- Ensures unique User ID for a given key
|
||||
|
||||
CONSTRAINT fk_pgp_uids_fingerprint
|
||||
FOREIGN KEY (fingerprint)
|
||||
REFERENCES pgp_keys (fingerprint)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Indexes for efficient lookups
|
||||
CREATE INDEX IF NOT EXISTS idx_pgp_uids_email ON pgp_uids (email);
|
||||
CREATE INDEX IF NOT EXISTS idx_pgp_uids_verified ON pgp_uids (verified);
|
||||
|
||||
-- For advanced text search on 'uid' (user_id_string) using LIKE or regex, consider pg_trgm.
|
||||
CREATE EXTENSION IF NOT EXISTS pg_trgm;
|
||||
CREATE INDEX IF NOT EXISTS idx_pgp_uids_uid_gin ON pgp_uids USING GIN (uid gin_trgm_ops);
|
||||
5
internal/service/mail/service.go
Normal file
5
internal/service/mail/service.go
Normal file
@ -0,0 +1,5 @@
|
||||
package mail
|
||||
|
||||
type MailServiceInterface interface {
|
||||
SendMail(to []string, msg []byte)
|
||||
}
|
||||
111
internal/service/storage/converter.go
Normal file
111
internal/service/storage/converter.go
Normal file
@ -0,0 +1,111 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
"github.com/ProtonMail/go-crypto/openpgp/armor"
|
||||
)
|
||||
|
||||
func extractEmail(fullNameWithEmail string) string {
|
||||
re := regexp.MustCompile(`<([^>]+)>`)
|
||||
matches := re.FindStringSubmatch(fullNameWithEmail)
|
||||
if len(matches) > 1 {
|
||||
return matches[1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ConverterPgpToService converts a raw openpgp.EntityList into a slice of service.PGPkey objects.
|
||||
// It parses key data, generates verification tokens for UIDs, and extracts email addresses.
|
||||
//
|
||||
// Parameters:
|
||||
//
|
||||
// entities: The openpgp.EntityList obtained from parsing an armored PGP block.
|
||||
//
|
||||
// Returns:
|
||||
//
|
||||
// []*PGPkey: A slice of converted PGPkey objects. Returns an empty (non-nil) slice if no valid entities are found.
|
||||
// error: An error if any entity fails to be processed or contains invalid data.
|
||||
func ConverterPgpToService(entities openpgp.EntityList) ([]*PGPkey, error) {
|
||||
var pgpKeys []*PGPkey
|
||||
|
||||
if entities == nil {
|
||||
return pgpKeys, nil
|
||||
}
|
||||
|
||||
for i, entity := range entities {
|
||||
// Test on correct data
|
||||
if entity == nil {
|
||||
log.Printf("Warning: ConverterPgpToService: encountered nil entity at index %d, skipping.", i)
|
||||
continue
|
||||
}
|
||||
if entity.PrimaryKey == nil {
|
||||
log.Printf("Warning: ConverterPgpToService: entity at index %d has no primary key, skipping.", i)
|
||||
continue
|
||||
}
|
||||
|
||||
publicKeyBuf := bytes.NewBuffer(nil)
|
||||
publicKeyWriter, err := armor.Encode(publicKeyBuf, openpgp.PublicKeyType, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converter error: entity %d: failed to create armor encoder: %w", i, err)
|
||||
}
|
||||
|
||||
if err = entity.Serialize(publicKeyWriter); err != nil {
|
||||
return nil, fmt.Errorf("converter error: entity %d: failed to serialize public key: %w", i, err)
|
||||
}
|
||||
publicKeyWriter.Close()
|
||||
|
||||
// Write it
|
||||
|
||||
pgpKey := PGPkey{
|
||||
Revoked: entity.Revoked(time.Now()),
|
||||
UpdateTime: entity.PrimaryKey.CreationTime.UTC(), // <--- FIX IS HERE! Use PrimaryKey's CreationTime
|
||||
Fingerprint: strings.ToUpper(hex.EncodeToString(entity.PrimaryKey.Fingerprint)),
|
||||
Packet: publicKeyBuf.String(),
|
||||
Uids: []*PGPUid{},
|
||||
}
|
||||
|
||||
// Process each UID for the current entity
|
||||
for _, uid := range entity.Identities {
|
||||
if uid == nil || uid.UserId == nil {
|
||||
log.Printf("Warning: ConverterPgpToService: encountered nil Identity or UserId for key %s, skipping.", pgpKey.Fingerprint)
|
||||
continue
|
||||
}
|
||||
if uid.Name == "" {
|
||||
log.Printf("Warning: ConverterPgpToService: encountered empty User ID string for key %s, skipping.", pgpKey.Fingerprint)
|
||||
continue
|
||||
}
|
||||
|
||||
randBytes := make([]byte, 16)
|
||||
if _, err := rand.Read(randBytes); err != nil {
|
||||
return nil, fmt.Errorf("converter error: key %s: failed to generate random bytes for token: %w", pgpKey.Fingerprint, err)
|
||||
}
|
||||
token := hex.EncodeToString(randBytes)
|
||||
|
||||
extractedEmail := extractEmail(uid.Name)
|
||||
if extractedEmail == "" {
|
||||
log.Printf("Warning: ConverterPgpToService: extracted empty email from UID '%s' for key %s, skipping UID.", uid.Name, pgpKey.Fingerprint)
|
||||
continue
|
||||
}
|
||||
|
||||
pgpKey.Uids = append(pgpKey.Uids, &PGPUid{
|
||||
Verify: false,
|
||||
UIDString: uid.Name,
|
||||
Email: extractedEmail,
|
||||
Token: token,
|
||||
TokenExpires: time.Now().Add(time.Hour * 5),
|
||||
Fingerprint: pgpKey.Fingerprint,
|
||||
})
|
||||
}
|
||||
pgpKeys = append(pgpKeys, &pgpKey)
|
||||
}
|
||||
return pgpKeys, nil
|
||||
}
|
||||
30
internal/service/storage/interface.go
Normal file
30
internal/service/storage/interface.go
Normal file
@ -0,0 +1,30 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
)
|
||||
|
||||
type StorageInterface interface {
|
||||
AddKey(openpgp.EntityList) error
|
||||
GetKeyByUID(uid string) openpgp.EntityList
|
||||
GetKeyByFingerprint(fngr string) openpgp.EntityList
|
||||
}
|
||||
|
||||
type PGPkey struct {
|
||||
Revoked bool
|
||||
UpdateTime time.Time
|
||||
Fingerprint string
|
||||
Packet string
|
||||
Uids []*PGPUid
|
||||
}
|
||||
|
||||
type PGPUid struct {
|
||||
Verify bool
|
||||
Token string
|
||||
TokenExpires time.Time
|
||||
Email string
|
||||
UIDString string // "Full Name (comment) <email@example.com>"
|
||||
Fingerprint string
|
||||
}
|
||||
153
internal/service/storage/serv/service.go
Normal file
153
internal/service/storage/serv/service.go
Normal file
@ -0,0 +1,153 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
storeRepo "gadrid/internal/repo/storage"
|
||||
service "gadrid/internal/service/storage"
|
||||
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
hkp "github.com/emersion/go-openpgp-hkp"
|
||||
)
|
||||
|
||||
type StorageService struct {
|
||||
hkp.Adder
|
||||
hkp.Lookuper
|
||||
// mailService interface{}
|
||||
storage storeRepo.StorageRepositotyInterface
|
||||
}
|
||||
|
||||
func New(storageRepository storeRepo.StorageRepositotyInterface) StorageService {
|
||||
return StorageService{
|
||||
storage: storageRepository,
|
||||
}
|
||||
}
|
||||
func (s StorageService) Add(el openpgp.EntityList) error {
|
||||
fmt.Println(el)
|
||||
pgpKeys, err := service.ConverterPgpToService(el) // Assuming ConverterPgpToService might return an error now.
|
||||
if err != nil {
|
||||
return fmt.Errorf("service error: Add: failed to convert PGP entities: %w", err)
|
||||
}
|
||||
|
||||
if len(pgpKeys) == 0 {
|
||||
return nil // No keys to add, nothing to do.
|
||||
}
|
||||
|
||||
// 2. Pass the context from this service method down to the storage layer.
|
||||
// 3. ALWAYS check the error returned by the storage layer.
|
||||
err = s.storage.AddKey(context.Background(), pgpKeys) // Pass context here
|
||||
if err != nil {
|
||||
return fmt.Errorf("service error: Add: failed to add keys to storage: %w", err)
|
||||
}
|
||||
|
||||
return nil // Successfully processed
|
||||
}
|
||||
|
||||
// Get key by fingerprint
|
||||
// In Specification "op=get" fingerprint and uid
|
||||
// FUTUE TE IPSUM specification! Only fingerprint search!
|
||||
|
||||
func (s StorageService) Get(req *hkp.LookupRequest) (openpgp.EntityList, error) {
|
||||
|
||||
req.Search = strings.Trim(req.Search, "0x")
|
||||
|
||||
if len(req.Search) <= 15 {
|
||||
return nil, fmt.Errorf("service error: Get: Low entropy string for search")
|
||||
}
|
||||
|
||||
dbKeys, err := s.storage.GetKey(context.Background(), req.Search)
|
||||
if err != nil {
|
||||
if errors.Is(err, storeRepo.ErrKeyNotFound) {
|
||||
return nil, fmt.Errorf("service error: Get: key %s not found: %w", req.Search, err)
|
||||
}
|
||||
return nil, fmt.Errorf("service error: Get: db request failed for key %s: %w", req.Search, err)
|
||||
}
|
||||
|
||||
if len(dbKeys) == 0 {
|
||||
return nil, fmt.Errorf("service error: Get: key %s not found (repository returned empty list): %w", req.Search, storeRepo.ErrKeyNotFound)
|
||||
}
|
||||
|
||||
dbKey := dbKeys[0]
|
||||
|
||||
filteredEntity, err := s.sanitizeAndFilterKey(dbKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("service error: Get: failed to sanitize and filter key %s: %w", dbKey.Fingerprint, err)
|
||||
}
|
||||
|
||||
return filteredEntity, nil
|
||||
}
|
||||
|
||||
func (s StorageService) Index(req *hkp.LookupRequest) ([]hkp.IndexKey, error) {
|
||||
|
||||
if len(req.Search) < 5 {
|
||||
return nil, fmt.Errorf("service error: Index: short string for search")
|
||||
}
|
||||
|
||||
dbKeys, err := s.storage.Index(context.Background(), req.Search) // Pass ctx here
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("service error: Index: db index request for '%s': %w", req.Search, err) // Include original error
|
||||
}
|
||||
|
||||
if len(dbKeys) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var respIndex []hkp.IndexKey
|
||||
for _, dbKey := range dbKeys {
|
||||
filteredEntity, err := s.sanitizeAndFilterKey(dbKey)
|
||||
if err != nil {
|
||||
log.Printf("Warning: service error: Index: failed to sanitize and filter key %s: %v, skipping.", dbKey.Fingerprint, err)
|
||||
continue // Skip this problematic key
|
||||
}
|
||||
|
||||
for _, fpk := range filteredEntity {
|
||||
idxKey, err := hkp.IndexKeyFromEntity(fpk)
|
||||
if err != nil {
|
||||
log.Printf("Warning: service error: Index: failed to convert entity %s to IndexKey: %v, skipping.", dbKey.Fingerprint, err)
|
||||
continue
|
||||
}
|
||||
respIndex = append(respIndex, *idxKey)
|
||||
}
|
||||
}
|
||||
|
||||
if len(respIndex) == 0 {
|
||||
return nil, fmt.Errorf("service error: Index: no valid index keys found after processing for '%s': %w", req.Search, storeRepo.ErrKeyNotFound)
|
||||
}
|
||||
|
||||
return respIndex, nil
|
||||
}
|
||||
|
||||
func (s StorageService) sanitizeAndFilterKey(dbKey *service.PGPkey) (openpgp.EntityList, error) {
|
||||
|
||||
parsedEntities, err := openpgp.ReadArmoredKeyRing(strings.NewReader(dbKey.Packet))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse key packet for %s: %w", dbKey.Fingerprint, err)
|
||||
}
|
||||
|
||||
if len(parsedEntities) == 0 {
|
||||
return nil, fmt.Errorf("parsed key packet for %s yielded no entities", dbKey.Fingerprint)
|
||||
}
|
||||
var respEntities openpgp.EntityList
|
||||
for _, e := range parsedEntities {
|
||||
if e == nil {
|
||||
return nil, fmt.Errorf("no entity with matching fingerprint %s found in parsed packet from DB", dbKey.Fingerprint)
|
||||
}
|
||||
verifiedUIDStringsFromDB := make(map[string]struct{})
|
||||
for _, dbUID := range dbKey.Uids {
|
||||
verifiedUIDStringsFromDB[dbUID.UIDString] = struct{}{}
|
||||
}
|
||||
filteredIdentities := make(map[string]*openpgp.Identity)
|
||||
for identityKey, identity := range e.Identities {
|
||||
if _, isVerified := verifiedUIDStringsFromDB[identity.Name]; isVerified {
|
||||
filteredIdentities[identityKey] = identity // Keep this identity
|
||||
}
|
||||
}
|
||||
e.Identities = filteredIdentities
|
||||
respEntities = append(respEntities, e)
|
||||
}
|
||||
return respEntities, nil
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user