diff --git a/config/config.go b/config/config.go index 222aa7b..d75c191 100644 --- a/config/config.go +++ b/config/config.go @@ -50,7 +50,9 @@ type ConfigAdmin struct { // The password has to be supplied in the `password` GET form value for all requests // to /profiling/* ProfilingPassword string `toml:"profiling_password"` - AllowRegistration bool `toml:"allow_registration"` + // Allow registration on the server + // If disabled, user must be manually created (currently via the debug server) + AllowRegistration bool `toml:"allow_registration"` } type ConfigStorage struct { @@ -103,15 +105,32 @@ type ConfigSelf struct { ServerDisplayName string `toml:"server_display_name"` } +// Contains experimental features that could be good to have +// but are either in an unstable implementation state +// or maybe not widely supported by other implementations +// +// All features controlled by this config section +// **MUST BE** +// disabled by default +type ConfigExperimental struct { + // Use ED25519 key pairs instead of RSA 2048 + // ED25519 keys are shorter and safer, but might not be supported by other + // implementations + // Both are created and stored for each local user. If this flag is enabled, + // Linstrom shares the ED25519 key on request, otherwise the RSA key + UseEd25519Keys bool `toml:"use_ed25519_keys"` +} + type Config struct { - General ConfigGeneral `toml:"general"` - SSL ConfigSSL `toml:"ssl"` - Admin ConfigAdmin `toml:"admin"` - Webauthn ConfigWebAuthn `toml:"webauthn"` - Storage ConfigStorage `toml:"storage"` - Mail ConfigMail `toml:"mail"` - Self ConfigSelf `toml:"self"` - S3 ConfigS3 `toml:"s3"` + General ConfigGeneral `toml:"general"` + SSL ConfigSSL `toml:"ssl"` + Admin ConfigAdmin `toml:"admin"` + Webauthn ConfigWebAuthn `toml:"webauthn"` + Storage ConfigStorage `toml:"storage"` + Mail ConfigMail `toml:"mail"` + Self ConfigSelf `toml:"self"` + S3 ConfigS3 `toml:"s3"` + Experimental ConfigExperimental `toml:"experimental"` } var GlobalConfig Config @@ -178,6 +197,9 @@ var defaultConfig Config = Config{ Endpoint: "http://localhost:3900", UseSSL: false, }, + Experimental: ConfigExperimental{ + UseEd25519Keys: false, + }, } func (gc *ConfigGeneral) GetFullDomain() string { diff --git a/devserver/linstrom.toml b/devserver/linstrom.toml index 970c676..47f9263 100644 --- a/devserver/linstrom.toml +++ b/devserver/linstrom.toml @@ -44,3 +44,6 @@ endpoint = "http://localhost:3900" use_ssl = false bucket_name = "linstrom-bucket" + +[experimental] + use_ed25519_keys = false diff --git a/go.mod b/go.mod index 6fb5f6a..be15928 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.23.0 toolchain go1.23.7 require ( - git.mstar.dev/mstar/goutils v1.12.0 + git.mstar.dev/mstar/goutils v1.12.1 github.com/BurntSushi/toml v1.4.0 github.com/dgraph-io/ristretto v0.2.0 github.com/eko/gocache/lib/v4 v4.1.6 @@ -24,6 +24,7 @@ require ( github.com/rs/zerolog v1.33.0 golang.org/x/crypto v0.36.0 golang.org/x/image v0.20.0 + golang.org/x/sys v0.32.0 gorm.io/driver/postgres v1.5.7 gorm.io/gen v0.3.26 gorm.io/gorm v1.25.12 @@ -32,6 +33,7 @@ require ( require ( filippo.io/edwards25519 v1.1.0 // indirect + git.mstar.dev/mstar/canvas v0.13.1 // indirect git.mstar.dev/mstar/goap v1.2.5 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect @@ -47,6 +49,7 @@ require ( github.com/go-webauthn/x v0.1.14 // indirect github.com/goccy/go-json v0.10.3 // indirect github.com/golang-jwt/jwt/v5 v5.2.1 // indirect + github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/mock v1.6.0 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/go-tpm v0.9.1 // indirect @@ -83,7 +86,6 @@ require ( golang.org/x/mod v0.17.0 // indirect golang.org/x/net v0.30.0 // indirect golang.org/x/sync v0.12.0 // indirect - golang.org/x/sys v0.32.0 // indirect golang.org/x/text v0.23.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect google.golang.org/protobuf v1.33.0 // indirect diff --git a/go.sum b/go.sum index 09774c7..c40e5d8 100644 --- a/go.sum +++ b/go.sum @@ -33,6 +33,8 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +git.mstar.dev/mstar/canvas v0.13.1 h1:+oJRv3O+1vDOqMQFXfV6r+o2JiZGBARadlWXOrK6WUo= +git.mstar.dev/mstar/canvas v0.13.1/go.mod h1:CzLWCvOvHXsLbwU9l8WBL/RU5VAorgJ9+Ald5yhWoMs= git.mstar.dev/mstar/goap v0.0.0-20250407153813-45fa095a1597 h1:KUdA5J1ArvD7X4z8ttE41xaCo+Hv/vWB8scoT+HIpOE= git.mstar.dev/mstar/goap v0.0.0-20250407153813-45fa095a1597/go.mod h1:cVCXcMGdYk8pySulIYSqMuvGG6lYEkibM5pPy97gylQ= git.mstar.dev/mstar/goap v1.2.0 h1:jiWgU7zria+uMEq40lyvrNqofmw4Voe+sRboxxcnbZE= @@ -57,6 +59,8 @@ git.mstar.dev/mstar/goutils v1.11.1 h1:G21MjZzQDnpC7h+ZkfITbqX+jBQtqZ4FB7rj4K6id git.mstar.dev/mstar/goutils v1.11.1/go.mod h1:juxY0eZEMnA95fedRp2LVXvUBgEjz66nE8SEdGKcxMA= git.mstar.dev/mstar/goutils v1.12.0 h1:d88hLS8KnLUCI+8aWBR6228M43hxHdJpj8WuSqm4LAM= git.mstar.dev/mstar/goutils v1.12.0/go.mod h1:juxY0eZEMnA95fedRp2LVXvUBgEjz66nE8SEdGKcxMA= +git.mstar.dev/mstar/goutils v1.12.1 h1:HZKKzMNfx7JKSUi5s8SwwUFEqEX6xvkM6NMf+Pht+lo= +git.mstar.dev/mstar/goutils v1.12.1/go.mod h1:juxY0eZEMnA95fedRp2LVXvUBgEjz66nE8SEdGKcxMA= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= @@ -150,6 +154,8 @@ github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0kt github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= +github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -392,6 +398,7 @@ golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2 golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20200801110659-972c09e46d76/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.20.0 h1:7cVCUjQwfL18gyBJOmYvptfSHS8Fb3YUDtfLIZ7Nbpw= golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= diff --git a/main.go b/main.go index b5a0902..1b193ee 100644 --- a/main.go +++ b/main.go @@ -160,6 +160,6 @@ func newServer() { }() } log.Info().Msg("Starting public server") - public := webpublic.New(":8080") + public := webpublic.New(":8080", &defaultDuck) public.Start() } diff --git a/shared/rsaKey.go b/shared/rsaKey.go deleted file mode 100644 index a29b5e4..0000000 --- a/shared/rsaKey.go +++ /dev/null @@ -1 +0,0 @@ -package shared diff --git a/shared/signing.go b/shared/signing.go new file mode 100644 index 0000000..568ddf1 --- /dev/null +++ b/shared/signing.go @@ -0,0 +1,49 @@ +package shared + +import ( + "crypto" + "crypto/ed25519" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" +) + +func GenerateKeypair(useEd bool) (publicKey []byte, privateKey []byte, err error) { + if useEd { + publicKey, privateKey, err := ed25519.GenerateKey(nil) + if err != nil { + return nil, nil, err + } + publicKeyBytes, err := x509.MarshalPKIXPublicKey(publicKey) + return publicKeyBytes, privateKey, nil + + } else { + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, nil, err + } + privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey) + publicKeyBytes, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey) + if err != nil { + return nil, nil, err + } + return publicKeyBytes, privateKeyBytes, nil + } +} + +func Sign(toSign string, keyBytes []byte, keyIsRsa bool) ([]byte, error) { + if keyIsRsa { + key, err := x509.ParsePKCS1PrivateKey(keyBytes) + if err != nil { + return nil, err + } + hash := sha256.Sum256([]byte(toSign)) + signed, err := key.Sign(rand.Reader, hash[:], crypto.SHA256) + return signed, err + } else { + key := ed25519.PrivateKey(keyBytes) + signed, err := key.Sign(rand.Reader, []byte(toSign), crypto.SHA256) + return signed, err + } +} diff --git a/storage-new/dbgen/users.gen.go b/storage-new/dbgen/users.gen.go index a71294a..cb7b476 100644 --- a/storage-new/dbgen/users.gen.go +++ b/storage-new/dbgen/users.gen.go @@ -40,14 +40,16 @@ func newUser(db *gorm.DB, opts ...gen.DOOption) user { _user.BackgroundId = field.NewField(tableName, "background_id") _user.BannerId = field.NewField(tableName, "banner_id") _user.Indexable = field.NewBool(tableName, "indexable") - _user.PublicKey = field.NewBytes(tableName, "public_key") + _user.PublicKeyRsa = field.NewBytes(tableName, "public_key_rsa") + _user.PublicKeyEd = field.NewBytes(tableName, "public_key_ed") _user.RestrictedFollow = field.NewBool(tableName, "restricted_follow") _user.Location = field.NewField(tableName, "location") _user.Birthday = field.NewField(tableName, "birthday") _user.Verified = field.NewBool(tableName, "verified") _user.PasskeyId = field.NewBytes(tableName, "passkey_id") _user.FinishedRegistration = field.NewBool(tableName, "finished_registration") - _user.PrivateKey = field.NewBytes(tableName, "private_key") + _user.PrivateKeyRsa = field.NewBytes(tableName, "private_key_rsa") + _user.PrivateKeyEd = field.NewBytes(tableName, "private_key_ed") _user.RemoteInfo = userHasOneRemoteInfo{ db: db.Session(&gorm.Session{}), @@ -353,14 +355,16 @@ type user struct { BackgroundId field.Field BannerId field.Field Indexable field.Bool - PublicKey field.Bytes + PublicKeyRsa field.Bytes + PublicKeyEd field.Bytes RestrictedFollow field.Bool Location field.Field Birthday field.Field Verified field.Bool PasskeyId field.Bytes FinishedRegistration field.Bool - PrivateKey field.Bytes + PrivateKeyRsa field.Bytes + PrivateKeyEd field.Bytes RemoteInfo userHasOneRemoteInfo InfoFields userHasManyInfoFields @@ -413,14 +417,16 @@ func (u *user) updateTableName(table string) *user { u.BackgroundId = field.NewField(table, "background_id") u.BannerId = field.NewField(table, "banner_id") u.Indexable = field.NewBool(table, "indexable") - u.PublicKey = field.NewBytes(table, "public_key") + u.PublicKeyRsa = field.NewBytes(table, "public_key_rsa") + u.PublicKeyEd = field.NewBytes(table, "public_key_ed") u.RestrictedFollow = field.NewBool(table, "restricted_follow") u.Location = field.NewField(table, "location") u.Birthday = field.NewField(table, "birthday") u.Verified = field.NewBool(table, "verified") u.PasskeyId = field.NewBytes(table, "passkey_id") u.FinishedRegistration = field.NewBool(table, "finished_registration") - u.PrivateKey = field.NewBytes(table, "private_key") + u.PrivateKeyRsa = field.NewBytes(table, "private_key_rsa") + u.PrivateKeyEd = field.NewBytes(table, "private_key_ed") u.fillFieldMap() @@ -437,7 +443,7 @@ func (u *user) GetFieldByName(fieldName string) (field.OrderExpr, bool) { } func (u *user) fillFieldMap() { - u.fieldMap = make(map[string]field.Expr, 33) + u.fieldMap = make(map[string]field.Expr, 35) u.fieldMap["id"] = u.ID u.fieldMap["username"] = u.Username u.fieldMap["created_at"] = u.CreatedAt @@ -451,14 +457,16 @@ func (u *user) fillFieldMap() { u.fieldMap["background_id"] = u.BackgroundId u.fieldMap["banner_id"] = u.BannerId u.fieldMap["indexable"] = u.Indexable - u.fieldMap["public_key"] = u.PublicKey + u.fieldMap["public_key_rsa"] = u.PublicKeyRsa + u.fieldMap["public_key_ed"] = u.PublicKeyEd u.fieldMap["restricted_follow"] = u.RestrictedFollow u.fieldMap["location"] = u.Location u.fieldMap["birthday"] = u.Birthday u.fieldMap["verified"] = u.Verified u.fieldMap["passkey_id"] = u.PasskeyId u.fieldMap["finished_registration"] = u.FinishedRegistration - u.fieldMap["private_key"] = u.PrivateKey + u.fieldMap["private_key_rsa"] = u.PrivateKeyRsa + u.fieldMap["private_key_ed"] = u.PrivateKeyEd } diff --git a/storage-new/models/User.go b/storage-new/models/User.go index 20b4cb0..ee5402b 100644 --- a/storage-new/models/User.go +++ b/storage-new/models/User.go @@ -47,9 +47,10 @@ type User struct { Background *MediaMetadata ` json:"-"` BackgroundId sql.NullString ` json:"background_id"` // ID of a media file used as background image Banner *MediaMetadata ` json:"-"` - BannerId sql.NullString ` json:"banner_id"` // ID of a media file used as banner - Indexable bool ` json:"indexable"` // Whether this account can be found by crawlers - PublicKey []byte ` json:"public_key"` // The public key of the account + BannerId sql.NullString ` json:"banner_id"` // ID of a media file used as banner + Indexable bool ` json:"indexable"` // Whether this account can be found by crawlers + PublicKeyRsa []byte ` json:"public_key_rsa"` // The public RSA key of the account + PublicKeyEd []byte ` json:"public_key_ed"` // The public Ed25519 key of the account // Whether this account restricts following // If true, the owner must approve of a follow request first RestrictedFollow bool ` json:"restricted_follow"` @@ -66,7 +67,8 @@ type User struct { // saved space is worth PasskeyId []byte `json:"-"` FinishedRegistration bool `json:"-"` // Whether this account has completed registration yet - PrivateKey []byte `json:"-"` + PrivateKeyRsa []byte `json:"-"` + PrivateKeyEd []byte `json:"-"` // ---- "Remote" linked values InfoFields []UserInfoField `json:"-"` diff --git a/storage-new/self.go b/storage-new/self.go index d98fb96..27f380b 100644 --- a/storage-new/self.go +++ b/storage-new/self.go @@ -2,8 +2,6 @@ package storage import ( "crypto/rand" - "crypto/rsa" - "crypto/x509" "database/sql" "git.mstar.dev/mstar/goutils/other" @@ -11,6 +9,7 @@ import ( "gorm.io/gorm" "git.mstar.dev/mstar/linstrom/config" + "git.mstar.dev/mstar/linstrom/shared" "git.mstar.dev/mstar/linstrom/storage-new/dbgen" "git.mstar.dev/mstar/linstrom/storage-new/models" ) @@ -27,7 +26,7 @@ func InsertSelf() error { if err != nil { return other.Error("storage", "failed to save/update self server", err) } - user, err := insertUser(server) + user, err := insertUser(server, duck) if err != nil { return other.Error("storage", "failed to save/update self user", err) } @@ -95,7 +94,10 @@ func insertServer(duck *models.MediaMetadata) (*models.RemoteServer, error) { return &server, nil } -func insertUser(server *models.RemoteServer) (*models.User, error) { +func insertUser( + server *models.RemoteServer, + duckMedia *models.MediaMetadata, +) (*models.User, error) { dbUser, err := dbgen.User.GetByUsername("linstrom") if err == nil { return dbUser, nil @@ -103,16 +105,14 @@ func insertUser(server *models.RemoteServer) (*models.User, error) { if err != gorm.ErrRecordNotFound { return nil, err } - // publicKey, privateKey, err := ed25519.GenerateKey(nil) - privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + publicEdKeyBytes, privateEdKeyBytes, err := shared.GenerateKeypair(true) if err != nil { return nil, err } - if err = privateKey.Validate(); err != nil { + publicRsaKeyBytes, privateRsaKeyBytes, err := shared.GenerateKeypair(false) + if err != nil { return nil, err } - privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey) - publicKeyBytes := x509.MarshalPKCS1PublicKey(&privateKey.PublicKey) pkeyId := make([]byte, 64) _, err = rand.Read(pkeyId) if err != nil { @@ -129,15 +129,17 @@ func insertUser(server *models.RemoteServer) (*models.User, error) { DisplayName: config.GlobalConfig.Self.ServerActorDisplayName, Description: "The default linstrom server user", IsBot: true, - Icon: nil, - IconId: sql.NullString{Valid: false}, + Icon: duckMedia, + IconId: sql.NullString{Valid: true, String: duckMedia.ID}, Background: nil, BackgroundId: sql.NullString{Valid: false}, Banner: nil, BannerId: sql.NullString{Valid: false}, Indexable: false, - PublicKey: publicKeyBytes, - PrivateKey: privateKeyBytes, + PublicKeyEd: publicEdKeyBytes, + PrivateKeyEd: privateEdKeyBytes, + PublicKeyRsa: publicRsaKeyBytes, + PrivateKeyRsa: privateRsaKeyBytes, Verified: true, FinishedRegistration: true, PasskeyId: pkeyId, diff --git a/web/debug/posts.go b/web/debug/posts.go index 179fd85..4afa51e 100644 --- a/web/debug/posts.go +++ b/web/debug/posts.go @@ -4,10 +4,9 @@ import ( "database/sql" "encoding/json" "errors" - "fmt" "net/http" - httputils "git.mstar.dev/mstar/goutils/http" + webutils "git.mstar.dev/mstar/goutils/http" "git.mstar.dev/mstar/goutils/sliceutils" "github.com/rs/zerolog/log" "gorm.io/gorm" @@ -27,15 +26,28 @@ func postAs(w http.ResponseWriter, r *http.Request) { data := Inbound{} err := dec.Decode(&data) if err != nil { - httputils.HttpErr(w, 0, "json decode failed", http.StatusBadRequest) + webutils.ProblemDetails( + w, + http.StatusBadRequest, + "/errors/bad-request-data", + "bad request data", + nil, + map[string]any{ + "sample": Inbound{ + Username: "bob", + Content: "Heya there, this is sample data", + }, + }, + ) return } user, err := dbgen.User.GetByUsername(data.Username) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - httputils.HttpErr(w, 0, "no user with that name", http.StatusNotFound) + webutils.ProblemDetailsStatusOnly(w, http.StatusNotFound) } else { log.Error().Err(err).Str("name", data.Username).Msg("Failed to find user") + webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) } return } @@ -69,13 +81,13 @@ func notesFrom(w http.ResponseWriter, r *http.Request) { if err != nil { log.Error().Err(err).Str("name", username).Msg("Failed to get user") storage.HandleReconnectError(err) - httputils.HttpErr(w, 0, "failed to get user", http.StatusInternalServerError) + webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) return } notes, err := dbgen.Note.GetNotesPaged(user.ID, 0, uint8(models.NOTE_TARGET_PUBLIC)) if err != nil { log.Error().Err(err).Str("name", username).Msg("Failed to get notes") - httputils.HttpErr(w, 0, "failed to get notes", http.StatusInternalServerError) + webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) return } publicNotes := sliceutils.Map(notes, func(t models.Note) webshared.Note { @@ -83,11 +95,5 @@ func notesFrom(w http.ResponseWriter, r *http.Request) { n.FromModel(&t) return n }) - jsonNotes, err := json.Marshal(publicNotes) - if err != nil { - log.Error().Err(err).Msg("Failed to marshal notes") - httputils.HttpErr(w, 0, "failed to marshal", http.StatusInternalServerError) - return - } - fmt.Fprint(w, string(jsonNotes)) + webutils.SendJson(w, publicNotes) } diff --git a/web/debug/users.go b/web/debug/users.go index 9e95dfb..3195537 100644 --- a/web/debug/users.go +++ b/web/debug/users.go @@ -2,19 +2,18 @@ package webdebug import ( "crypto/rand" - "crypto/rsa" - "crypto/x509" "database/sql" "encoding/json" - "fmt" "net/http" "strconv" "time" - httputils "git.mstar.dev/mstar/goutils/http" + webutils "git.mstar.dev/mstar/goutils/http" + "git.mstar.dev/mstar/goutils/other" "git.mstar.dev/mstar/goutils/sliceutils" "github.com/rs/zerolog/log" + "git.mstar.dev/mstar/linstrom/shared" "git.mstar.dev/mstar/linstrom/storage-new/dbgen" "git.mstar.dev/mstar/linstrom/storage-new/models" webshared "git.mstar.dev/mstar/linstrom/web/shared" @@ -27,25 +26,27 @@ func getNonDeletedUsers(w http.ResponseWriter, r *http.Request) { var err error page, err = strconv.Atoi(pageStr) if err != nil { - httputils.HttpErr(w, 0, "page is not a number", http.StatusBadRequest) + webutils.HttpErr(w, 0, "page is not a number", http.StatusBadRequest) return } } users, err := dbgen.User.GetPagedAllNonDeleted(uint(page)) if err != nil { - httputils.HttpErr(w, 0, "failed to get users", http.StatusInternalServerError) + webutils.ProblemDetails( + w, + http.StatusInternalServerError, + "/errors/db-failure", + "database failure", + nil, + nil, + ) return } - marshalled, err := json.Marshal(sliceutils.Map(users, func(t models.User) webshared.User { + webutils.SendJson(w, sliceutils.Map(users, func(t models.User) webshared.User { u := webshared.User{} u.FromModel(&t) return u })) - if err != nil { - httputils.HttpErr(w, 0, "failed to marshal users", http.StatusInternalServerError) - return - } - fmt.Fprint(w, string(marshalled)) } func createLocalUser(w http.ResponseWriter, r *http.Request) { @@ -61,19 +62,43 @@ func createLocalUser(w http.ResponseWriter, r *http.Request) { data := Inbound{} err := jsonDecoder.Decode(&data) if err != nil { - httputils.HttpErr(w, 0, "decode failed", http.StatusBadRequest) + webutils.ProblemDetails( + w, + http.StatusBadRequest, + "/errors/bad-request-data", + "bad request data", + nil, + map[string]any{ + "sample": Inbound{ + Username: "bob", + Displayname: "Bob Bobbington", + Description: "Bobbing Bobs bop to Bobs bobbing beats", + Birthday: other.IntoPointer(time.Now()), + Location: nil, + IsBot: false, + }, + }, + ) return } - // publicKey, privateKey, err := ed25519.GenerateKey(nil) - privateKey, err := rsa.GenerateKey(rand.Reader, 2048) - privateKeyBytes := x509.MarshalPKCS1PrivateKey(privateKey) - publicKeyBytes := x509.MarshalPKCS1PublicKey(&privateKey.PublicKey) + publicKeyEdBytes, privateKeyEdBytes, err := shared.GenerateKeypair(true) + if err != nil { + log.Error().Err(err).Msg("Failed to generate and marshal public key") + webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) + return + } + publicKeyRsaBytes, privateKeyRsaBytes, err := shared.GenerateKeypair(false) + if err != nil { + log.Error().Err(err).Msg("Failed to generate and marshal public key") + webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) + return + } pkeyId := make([]byte, 64) _, err = rand.Read(pkeyId) if err != nil { log.Error().Err(err).Msg("Failed to generate passkey id") - httputils.HttpErr(w, 0, "failed to generate passkey id", http.StatusInternalServerError) + webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) return } @@ -84,8 +109,10 @@ func createLocalUser(w http.ResponseWriter, r *http.Request) { u.Description, u.IsBot, u.ServerId, - u.PrivateKey, - u.PublicKey, + u.PrivateKeyEd, + u.PublicKeyEd, + u.PrivateKeyRsa, + u.PublicKeyRsa, u.PasskeyId, ) if data.Birthday != nil { @@ -95,14 +122,16 @@ func createLocalUser(w http.ResponseWriter, r *http.Request) { query = query.Select(u.Location) } user := models.User{ - Username: data.Username, - DisplayName: data.Displayname, - Description: data.Description, - IsBot: data.IsBot, - ServerId: 1, // Hardcoded, Self is always first ID - PublicKey: publicKeyBytes, - PrivateKey: privateKeyBytes, - PasskeyId: pkeyId, + Username: data.Username, + DisplayName: data.Displayname, + Description: data.Description, + IsBot: data.IsBot, + ServerId: 1, // Hardcoded, Self is always first ID + PublicKeyRsa: publicKeyRsaBytes, + PublicKeyEd: publicKeyEdBytes, + PrivateKeyRsa: privateKeyRsaBytes, + PrivateKeyEd: privateKeyEdBytes, + PasskeyId: pkeyId, } if data.Birthday != nil { user.Birthday = sql.NullTime{Valid: true, Time: *data.Birthday} @@ -112,7 +141,7 @@ func createLocalUser(w http.ResponseWriter, r *http.Request) { } if err = u.Create(&user); err != nil { log.Error().Err(err).Msg("failed to create new local user") - httputils.HttpErr(w, 0, "db failure", http.StatusInternalServerError) + webutils.ProblemDetailsStatusOnly(w, http.StatusInternalServerError) } } diff --git a/web/public/api/activitypub/activitypub.go b/web/public/api/activitypub/activitypub.go index 8b64bea..d1bae44 100644 --- a/web/public/api/activitypub/activitypub.go +++ b/web/public/api/activitypub/activitypub.go @@ -8,6 +8,7 @@ import ( func BuildActivitypubRouter() http.Handler { router := http.NewServeMux() router.HandleFunc("/user/{id}", users) + router.HandleFunc("/user/{id}/inbox", userInbox) router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "in ap") }) diff --git a/web/public/api/activitypub/user.go b/web/public/api/activitypub/user.go index 68669f5..f5f8689 100644 --- a/web/public/api/activitypub/user.go +++ b/web/public/api/activitypub/user.go @@ -3,13 +3,17 @@ package activitypub import ( "encoding/json" "fmt" + "io" "net/http" + "time" webutils "git.mstar.dev/mstar/goutils/http" - "github.com/rs/zerolog/log" + "github.com/rs/zerolog/hlog" + "git.mstar.dev/mstar/linstrom/config" "git.mstar.dev/mstar/linstrom/storage-new" "git.mstar.dev/mstar/linstrom/storage-new/dbgen" + webshared "git.mstar.dev/mstar/linstrom/web/shared" ) var baseLdContext = []any{ @@ -23,17 +27,30 @@ func users(w http.ResponseWriter, r *http.Request) { Owner string `json:"owner"` Pem string `json:"publicKeyPem"` } - type Outbound struct { - Context []any `json:"@context"` - Id string `json:"id"` - Type string `json:"type"` - PreferredUsername string `json:"preferredUsername"` - Inbox string `json:"inbox"` - // FIXME: Public key stuff is borken. Focus on fixing - // PublicKey OutboundKey `json:"publicKey"` + type OutboundMedia struct { + Type string `json:"type"` + Url string `json:"url"` + MediaType string `json:"mediaType"` } + type Outbound struct { + Context []any `json:"@context"` + Id string `json:"id"` + Type string `json:"type"` + PreferredUsername string `json:"preferredUsername"` + Inbox string `json:"inbox"` + PublicKey OutboundKey `json:"publicKey"` + Published time.Time `json:"published"` + DisplayName string `json:"name"` + Description *string `json:"summary,omitempty"` + PublicUrl string `json:"url"` + Icon *OutboundMedia `json:"icon,omitempty"` + Banner *OutboundMedia `json:"image,omitempty"` + } + log := hlog.FromRequest(r) userId := r.PathValue("id") - user, err := dbgen.User.Where(dbgen.User.ID.Eq(userId)).First() + user, err := dbgen.User.Where(dbgen.User.ID.Eq(userId)). + Preload(dbgen.User.Icon).Preload(dbgen.User.Banner). + First() if err != nil { webutils.ProblemDetails(w, 500, "/errors/db-failure", "internal database failure", nil, nil) if storage.HandleReconnectError(err) { @@ -43,20 +60,47 @@ func users(w http.ResponseWriter, r *http.Request) { } return } - // fmt.Println(x509.ParsePKCS1PublicKey(user.PublicKey)) apUrl := userIdToApUrl(user.ID) + var keyBytes string + if config.GlobalConfig.Experimental.UseEd25519Keys { + keyBytes = keyBytesToPem(user.PublicKeyEd) + } else { + keyBytes = keyBytesToPem(user.PublicKeyRsa) + } data := Outbound{ Context: baseLdContext, Id: apUrl, Type: "Person", - PreferredUsername: user.DisplayName, + PreferredUsername: user.Username, Inbox: apUrl + "/inbox", - // PublicKey: OutboundKey{ - // Id: apUrl + "#main-key", - // Owner: apUrl, - // Pem: keyBytesToPem(user.PublicKey), - // }, + PublicKey: OutboundKey{ + Id: apUrl + "#main-key", + Owner: apUrl, + Pem: keyBytes, + }, + Published: user.CreatedAt, + DisplayName: user.DisplayName, + PublicUrl: config.GlobalConfig.General.GetFullPublicUrl() + "/user/" + user.Username, + } + if user.Description != "" { + data.Description = &user.Description + } + if user.Icon != nil { + log.Debug().Msg("icon found") + data.Icon = &OutboundMedia{ + Type: "Image", + Url: webshared.EnsurePublicUrl(user.Icon.Location), + MediaType: user.Icon.Type, + } + } + if user.Banner != nil { + log.Debug().Msg("icon banner") + data.Banner = &OutboundMedia{ + Type: "Image", + Url: webshared.EnsurePublicUrl(user.Banner.Location), + MediaType: user.Banner.Type, + } } encoded, err := json.Marshal(data) @@ -64,6 +108,13 @@ func users(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, string(encoded)) } +func userInbox(w http.ResponseWriter, r *http.Request) { + log := hlog.FromRequest(r) + userId := r.PathValue("id") + data, err := io.ReadAll(r.Body) + log.Info().Err(err).Str("userId", userId).Bytes("body", data).Msg("Inbox message") +} + /* Fine. You win JsonLD. I can't get you to work properly. I'll just treat you like normal json then Fuck you. diff --git a/web/public/errorpages.go b/web/public/errorpages.go index 74be297..5743ad1 100644 --- a/web/public/errorpages.go +++ b/web/public/errorpages.go @@ -10,6 +10,7 @@ import ( var errorDescriptions = map[string]string{ "webfinger-bad-resource": "The given format for the \"resource\" url parameter was missing or invalid. It must follow the form \"acct:@\"", "db-failure": "The database query for this request failed for an undisclosed reason. This is often caused by bad input data conflicting with existing information. Try to submit different data or wait for some time", + "bad-request-data": "The data provided in the request doesn't match the requirements, see problem details' detail field for more information", } func errorTypeHandler(w http.ResponseWriter, r *http.Request) { diff --git a/web/public/server.go b/web/public/server.go index 45350b9..2fa9237 100644 --- a/web/public/server.go +++ b/web/public/server.go @@ -40,7 +40,7 @@ type Server struct { server *http.Server } -func New(addr string) *Server { +func New(addr string, duckImg *string) *Server { handler := http.NewServeMux() handler.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(500) @@ -51,6 +51,11 @@ func New(addr string) *Server { handler.HandleFunc("GET /.well-known/nodeinfo", api.WellKnownNodeinfo) handler.HandleFunc("GET /nodeinfo/2.1", api.Nodeinfo) handler.HandleFunc("GET /errors/{name}", errorTypeHandler) + handler.HandleFunc("GET /default-image", func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("Content-Type", "image/web") + w.Header().Add("Content-Disposition", "attachment; filename=\"duck.webp\"") + fmt.Fprint(w, *duckImg) + }) server := http.Server{ Handler: webutils.ChainMiddlewares(handler, webutils.LoggingMiddleware), Addr: addr, diff --git a/web/shared/User.go b/web/shared/User.go index 01775ba..1858a17 100644 --- a/web/shared/User.go +++ b/web/shared/User.go @@ -4,6 +4,7 @@ import ( "slices" "time" + "git.mstar.dev/mstar/linstrom/config" "git.mstar.dev/mstar/linstrom/shared" "git.mstar.dev/mstar/linstrom/storage-new/models" ) @@ -97,7 +98,11 @@ func (u *User) FromModel(m *models.User) { u.BannerId = &m.IconId.String } u.Indexable = m.Indexable - u.PublicKey = append(u.PublicKey, m.PublicKey...) + if config.GlobalConfig.Experimental.UseEd25519Keys { + u.PublicKey = append(u.PublicKey, m.PublicKeyEd...) + } else { + u.PublicKey = append(u.PublicKey, m.PublicKeyRsa...) + } u.RestrictedFollow = m.RestrictedFollow if m.Location.Valid { u.Location = &m.Location.String diff --git a/web/shared/linstromUrlType.go b/web/shared/linstromUrlType.go new file mode 100644 index 0000000..30949a7 --- /dev/null +++ b/web/shared/linstromUrlType.go @@ -0,0 +1,16 @@ +package webshared + +import "strings" + +// TODO: Define linstrom uri type + +var hardcodedUrls = map[string]string{ + "default-media": "/default-image", +} + +func EnsurePublicUrl(rawUrl string) string { + if !strings.HasPrefix(rawUrl, "linstrom://") { + return rawUrl + } + return strings.Replace(rawUrl, "linstrom://", "/", 1) +} diff --git a/web/shared/signing.go b/web/shared/signing.go new file mode 100644 index 0000000..3fd8e58 --- /dev/null +++ b/web/shared/signing.go @@ -0,0 +1,40 @@ +package webshared + +import ( + "strings" + + "git.mstar.dev/mstar/linstrom/shared" +) + +func CreateSignatureRSA( + method string, + target string, + headers map[string]string, + privateKeyBytes []byte, +) (string, error) { + message := genPreSignatureString(method, target, headers) + signed, err := shared.Sign(message, privateKeyBytes, true) + return string(signed), err +} + +func CreateSignatureED( + method string, + target string, + headers map[string]string, + privateKeyBytes []byte, +) (string, error) { + message := genPreSignatureString(method, target, headers) + signed, err := shared.Sign(message, privateKeyBytes, false) + return string(signed), err +} + +func genPreSignatureString(method, target string, headers map[string]string) string { + dataBuilder := strings.Builder{} + dataBuilder.WriteString("(request-target) ") + dataBuilder.WriteString(strings.ToLower(method) + " ") + dataBuilder.WriteString(target + "\n") + for k, v := range headers { + dataBuilder.WriteString(k + ": " + v + "\n") + } + return dataBuilder.String() +}