1
0
Fork 0
forked from jriou/coller

feat: Disable levels of encryptions by default

- Add `allow_client_encryption_key` option to allow encryption key provided by
  the client on the web UI (false by default)
- Add `allow_no_encryption` option to allow notes without encryption (disabled
  by default)

Signed-off-by: Julien Riou <julien@riou.xyz>
This commit is contained in:
Julien Riou 2025-09-24 17:44:50 +02:00
commit 61ca30690b
Signed by: jriou
GPG key ID: 9A099EDA51316854
6 changed files with 105 additions and 47 deletions

View file

@ -20,6 +20,8 @@ The file format is **JSON**:
* **database_dsn** (string): Connection string for the database (default "collerd.db") * **database_dsn** (string): Connection string for the database (default "collerd.db")
* **node_id** (int): Number between 0 and 1023 to define the node generating identifiers (see [snowflake](https://github.com/bwmarrin/snowflake)) * **node_id** (int): Number between 0 and 1023 to define the node generating identifiers (see [snowflake](https://github.com/bwmarrin/snowflake))
* **encryption_key_length** (int): Number of characters for generated encryption key (default 16) * **encryption_key_length** (int): Number of characters for generated encryption key (default 16)
* **allow_client_encryption_key** (bool): Allow encryption key provided by the client on the web UI
* **allow_no_encryption** (bool): Allow notes without encryption
* **expiration_interval** (int): Number of seconds to wait between two expiration runs * **expiration_interval** (int): Number of seconds to wait between two expiration runs
* **listen_address** (string): Address to listen for the web server (default "0.0.0.0") * **listen_address** (string): Address to listen for the web server (default "0.0.0.0")
* **listen_port** (int): Port to listen for the web server (default 8080) * **listen_port** (int): Port to listen for the web server (default 8080)

View file

@ -7,28 +7,30 @@ import (
) )
type Config struct { type Config struct {
Title string `json:"title"` Title string `json:"title"`
DatabaseType string `json:"database_type"` DatabaseType string `json:"database_type"`
DatabaseDsn string `json:"database_dsn"` DatabaseDsn string `json:"database_dsn"`
NodeID int64 `json:"node_id"` NodeID int64 `json:"node_id"`
EncryptionKeyLength int `json:"encryption_key_length"` EncryptionKeyLength int `json:"encryption_key_length"`
ExpirationInterval int `json:"expiration_interval"` AllowClientEncryptionKey bool `json:"allow_client_encryption_key"`
ListenAddress string `json:"listen_address"` AllowNoEncryption bool `json:"allow_no_encryption"`
ListenPort int `json:"listen_port"` ExpirationInterval int `json:"expiration_interval"`
Expirations []int `json:"expirations"` ListenAddress string `json:"listen_address"`
Expiration int `json:"expiration"` ListenPort int `json:"listen_port"`
MaxUploadSize int64 `json:"max_upload_size"` Expirations []int `json:"expirations"`
ShowVersion bool `json:"show_version"` Expiration int `json:"expiration"`
EnableMetrics bool `json:"enable_metrics"` MaxUploadSize int64 `json:"max_upload_size"`
PrometheusRoute string `json:"prometheus_route"` ShowVersion bool `json:"show_version"`
PrometheusNotesMetric string `json:"prometheus_notes_metric"` EnableMetrics bool `json:"enable_metrics"`
ObservationInterval int `json:"observation_internal"` PrometheusRoute string `json:"prometheus_route"`
Languages []string `json:"languages"` PrometheusNotesMetric string `json:"prometheus_notes_metric"`
Language string `json:"language"` ObservationInterval int `json:"observation_internal"`
EnableUploadFileButton bool `json:"enable_upload_file_button"` Languages []string `json:"languages"`
TLSCertFile string `json:"tls_cert_file"` Language string `json:"language"`
TLSKeyFile string `json:"tls_key_file"` EnableUploadFileButton bool `json:"enable_upload_file_button"`
BootstrapDirectory string `json:"bootstrap_directory"` TLSCertFile string `json:"tls_cert_file"`
TLSKeyFile string `json:"tls_key_file"`
BootstrapDirectory string `json:"bootstrap_directory"`
} }
func NewConfig() *Config { func NewConfig() *Config {

View file

@ -16,9 +16,11 @@ func HealthHandler(w http.ResponseWriter, r *http.Request) {
} }
type CreateNoteHandler struct { type CreateNoteHandler struct {
logger *slog.Logger logger *slog.Logger
db *Database db *Database
maxUploadSize int64 maxUploadSize int64
allowClientEncryptionKey bool
allowNoEncryption bool
} }
type CreateNotePayload struct { type CreateNotePayload struct {
@ -47,6 +49,16 @@ func (h *CreateNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return return
} }
if !h.allowNoEncryption && !body.Encrypted {
WriteError(w, "could not create note", fmt.Errorf("encryption is mandatory"))
return
}
if !h.allowClientEncryptionKey && body.EncryptionKey != "" {
WriteError(w, "could not create note", fmt.Errorf("client encryption key is not allowed"))
return
}
content, err := internal.Decode(body.Content) content, err := internal.Decode(body.Content)
if err != nil { if err != nil {

View file

@ -17,16 +17,18 @@ import (
) )
type PageData struct { type PageData struct {
Title string Title string
Version string Version string
Expirations []int Expirations []int
Expiration int Expiration int
Languages []string Languages []string
Err error Err error
URL string URL string
Note *Note Note *Note
EnableUploadFileButton bool EnableUploadFileButton bool
BootstrapDirectory string AllowClientEncryptionKey bool
AllowNoEncryption bool
BootstrapDirectory string
} }
type HomeHandler struct { type HomeHandler struct {
@ -115,7 +117,17 @@ func (h *CreateNoteWithFormHandler) ServeHTTP(w http.ResponseWriter, r *http.Req
deleteAfterRead := r.FormValue("delete-after-read") deleteAfterRead := r.FormValue("delete-after-read")
language := r.FormValue("language") language := r.FormValue("language")
if encryptionKey == "" && noEncryption == "" { if !h.PageData.AllowNoEncryption && noEncryption != "" {
h.PageData.Err = fmt.Errorf("encryption is mandatory")
h.Templates.ExecuteTemplate(w, templateName, h.PageData)
}
if !h.PageData.AllowClientEncryptionKey && encryptionKey != "" {
h.PageData.Err = fmt.Errorf("client encryption key is not allowed")
h.Templates.ExecuteTemplate(w, templateName, h.PageData)
}
if !h.PageData.AllowClientEncryptionKey && encryptionKey == "" && noEncryption == "" {
h.logger.Debug("generating encryption key") h.logger.Debug("generating encryption key")
encryptionKey = internal.GenerateChars(encryptionKeyLength) encryptionKey = internal.GenerateChars(encryptionKeyLength)
} }

View file

@ -99,9 +99,26 @@ func (s *Server) Start() error {
} }
// API // API
r.Path("/api/note").Handler(&CreateNoteHandler{logger: s.logger, db: s.db, maxUploadSize: s.config.MaxUploadSize}).Methods("POST") createNoteHandler := &CreateNoteHandler{
r.Path("/{id:[a-zA-Z0-9]+}/{encryptionKey:[a-zA-Z0-9]+}").Handler(&GetEncryptedNoteHandler{logger: s.logger, db: s.db}).Methods("GET") logger: s.logger,
r.Path("/{id:[a-zA-Z0-9]+}").Handler(&GetNoteHandler{logger: s.logger, db: s.db}).Methods("GET") db: s.db,
maxUploadSize: s.config.MaxUploadSize,
allowClientEncryptionKey: s.config.AllowClientEncryptionKey,
allowNoEncryption: s.config.AllowNoEncryption,
}
r.Path("/api/note").Handler(createNoteHandler).Methods("POST")
getEncryptedNoteHandler := &GetEncryptedNoteHandler{
logger: s.logger,
db: s.db,
}
r.Path("/{id:[a-zA-Z0-9]+}/{encryptionKey:[a-zA-Z0-9]+}").Handler(getEncryptedNoteHandler).Methods("GET")
getNoteHandler := &GetNoteHandler{
logger: s.logger,
db: s.db,
}
r.Path("/{id:[a-zA-Z0-9]+}").Handler(getNoteHandler).Methods("GET")
// Web pages // Web pages
funcs := template.FuncMap{ funcs := template.FuncMap{
@ -111,22 +128,25 @@ func (s *Server) Start() error {
"string": func(b []byte) string { return string(b) }, "string": func(b []byte) string { return string(b) },
} }
p := PageData{ p := PageData{
Title: s.config.Title, Title: s.config.Title,
Expirations: s.config.Expirations, Expirations: s.config.Expirations,
Expiration: s.config.Expiration, Expiration: s.config.Expiration,
Languages: s.config.Languages, Languages: s.config.Languages,
BootstrapDirectory: s.config.BootstrapDirectory, BootstrapDirectory: s.config.BootstrapDirectory,
EnableUploadFileButton: s.config.EnableUploadFileButton,
AllowClientEncryptionKey: s.config.AllowClientEncryptionKey,
AllowNoEncryption: s.config.AllowNoEncryption,
} }
if s.config.ShowVersion { if s.config.ShowVersion {
p.Version = s.version p.Version = s.version
} }
p.EnableUploadFileButton = s.config.EnableUploadFileButton
templates, err := template.New("templates").Funcs(funcs).ParseFS(templatesFS, "templates/*.html") templates, err := template.New("templates").Funcs(funcs).ParseFS(templatesFS, "templates/*.html")
if err != nil { if err != nil {
return err return err
} }
createNoteWithFormHandler := &CreateNoteWithFormHandler{ createNoteWithFormHandler := &CreateNoteWithFormHandler{
Templates: templates, Templates: templates,
PageData: p, PageData: p,
@ -142,7 +162,12 @@ func (s *Server) Start() error {
logger: s.logger, logger: s.logger,
} }
r.Path("/clients.html").Handler(clientsHandler).Methods("GET") r.Path("/clients.html").Handler(clientsHandler).Methods("GET")
r.Path("/clients/{os:[a-z]+}-{arch:[a-z0-9]+}/{clientName:[a-z]+}").Handler(&ClientHandler{logger: s.logger, version: p.Version}).Methods("GET")
clientHandler := &ClientHandler{
logger: s.logger,
version: p.Version,
}
r.Path("/clients/{os:[a-z]+}-{arch:[a-z0-9]+}/{clientName:[a-z]+}").Handler(clientHandler).Methods("GET")
encryptedWebNoteHandler := &GetEncryptedWebNoteHandler{ encryptedWebNoteHandler := &GetEncryptedWebNoteHandler{
Templates: templates, Templates: templates,

View file

@ -13,6 +13,7 @@
</div> </div>
<div class="container text-center justify-content-center w-75 mb-4"> <div class="container text-center justify-content-center w-75 mb-4">
<div class="row align-items-center"> <div class="row align-items-center">
{{if .AllowClientEncryptionKey}}
<div class="col-1"> <div class="col-1">
<label class="col-form-label col-form-label-sm" for="encryption-key">Encryption key</label> <label class="col-form-label col-form-label-sm" for="encryption-key">Encryption key</label>
</div> </div>
@ -21,11 +22,14 @@
title="Letters and numbers with length from 16 to 256" class="form-control" id="encryption-key" title="Letters and numbers with length from 16 to 256" class="form-control" id="encryption-key"
name="encryption-key"> name="encryption-key">
</div> </div>
{{end}}
{{if .AllowNoEncryption}}
<div class="col-1"> <div class="col-1">
<input type="checkbox" class="form-check-input" for="no-encryption-key" id="no-encryption-key" <input type="checkbox" class="form-check-input" for="no-encryption-key" id="no-encryption-key"
value="no-encryption-key" name="no-encryption-key"> value="no-encryption-key" name="no-encryption-key">
<label class="col-form-label col-form-label-sm" for="no-encryption-key">No encryption</label> <label class="col-form-label col-form-label-sm" for="no-encryption-key">No encryption</label>
</div> </div>
{{end}}
<div class="col-1"> <div class="col-1">
<input type="checkbox" class="form-check-input" for="delete-after-read" id="delete-after-read" <input type="checkbox" class="form-check-input" for="delete-after-read" id="delete-after-read"
value="delete-after-read" name="delete-after-read"> value="delete-after-read" name="delete-after-read">
@ -40,7 +44,8 @@
<select class="form-select" aria-label="Expiration" id="expiration" name="expiration"> <select class="form-select" aria-label="Expiration" id="expiration" name="expiration">
<option disabled>Expiration</option> <option disabled>Expiration</option>
{{range $exp := .Expirations}} {{range $exp := .Expirations}}
<option {{ if eq $exp $.Expiration }}selected="selected"{{end}} value="{{$exp}}">{{HumanDuration $exp}}</option> <option {{ if eq $exp $.Expiration }}selected="selected" {{end}} value="{{$exp}}">
{{HumanDuration $exp}}</option>
{{end}} {{end}}
</select> </select>
</div> </div>