feat: Rename password by encryption key
All checks were successful
/ pre-commit (push) Successful in 1m9s

Signed-off-by: Julien Riou <julien@riou.xyz>
This commit is contained in:
Julien Riou 2025-09-24 07:09:01 +02:00
commit 8e1dd686d3
Signed by: jriou
GPG key ID: 9A099EDA51316854
16 changed files with 118 additions and 117 deletions

View file

@ -22,17 +22,17 @@ Create from file:
coller -file filename.txt coller -file filename.txt
``` ```
Provide password for encryption: Provide encryption key:
``` ```
coller -ask-password coller -ask-encryption-key
coller -password PASSWORD coller -encryption-key ENCRYPTION_KEY
``` ```
Create public note: Create a note in cleartext:
``` ```
coller -no-password coller -no-encryption
``` ```
Return the copier command to use client-side decryption instead of the URL: Return the copier command to use client-side decryption instead of the URL:

View file

@ -64,10 +64,10 @@ func handleMain() int {
configFile := flag.String("config", filepath.Join(homeDir, ".config", AppName+".json"), "Configuration file") configFile := flag.String("config", filepath.Join(homeDir, ".config", AppName+".json"), "Configuration file")
reconfigure := flag.Bool("reconfigure", false, "Re-create configuration file") reconfigure := flag.Bool("reconfigure", false, "Re-create configuration file")
url := flag.String("url", "", "URL of the coller API") url := flag.String("url", "", "URL of the coller API")
password := flag.String("password", os.Getenv("COLLER_PASSWORD"), "Password to encrypt the note") encryptionKey := flag.String("encryption-key", os.Getenv("COLLER_ENCRYPTION_KEY"), "Key to encrypt the note")
askPassword := flag.Bool("ask-password", false, "Read password from input") askEncryptionKey := flag.Bool("ask-encryption-key", false, "Read encryption key from input")
noPassword := flag.Bool("no-password", false, "Allow notes without password") noEncryption := flag.Bool("no-encryption", false, "Allow notes without encryption key")
passwordLength := flag.Int("password-length", 16, "Length of the auto-generated password") encryptionKeyLength := flag.Int("encryption-key-length", 16, "Length of the auto-generated encryption key")
flag.StringVar(&fileName, "file", "", "Read content of the note from a file") flag.StringVar(&fileName, "file", "", "Read content of the note from a file")
expiration := flag.Int("expiration", 0, "Number of seconds before expiration") expiration := flag.Int("expiration", 0, "Number of seconds before expiration")
deleteAfterRead := flag.Bool("delete-after-read", false, "Delete the note after the first read") deleteAfterRead := flag.Bool("delete-after-read", false, "Delete the note after the first read")
@ -140,22 +140,22 @@ func handleMain() int {
content = clipboard.Read(clipboard.FmtText) content = clipboard.Read(clipboard.FmtText)
} }
if *askPassword { if *askEncryptionKey {
fmt.Print("Password: ") fmt.Print("Encryption key: ")
p, err := term.ReadPassword(int(syscall.Stdin)) p, err := term.ReadPassword(int(syscall.Stdin))
if err != nil { if err != nil {
return internal.ReturnError(logger, "could not read password", err) return internal.ReturnError(logger, "could not read encryption key", err)
} }
*password = string(p) *encryptionKey = string(p)
fmt.Print("\n") fmt.Print("\n")
} }
if !*noPassword && *password == "" { if !*noEncryption && *encryptionKey == "" {
logger.Debug("generating random password") logger.Debug("generating random encryption key")
if *passwordLength < internal.MIN_PASSWORD_LENGTH || *passwordLength > internal.MAX_PASSWORD_LENGTH { if *encryptionKeyLength < internal.MIN_ENCRYPTION_KEY_LENGTH || *encryptionKeyLength > internal.MAX_ENCRYPTION_KEY_LENGTH {
return internal.ReturnError(logger, "invalid password length for auto-generated password", fmt.Errorf("password length must be between %d and %d", internal.MIN_PASSWORD_LENGTH, internal.MAX_PASSWORD_LENGTH)) return internal.ReturnError(logger, "invalid length of auto-generated encryption key", fmt.Errorf("encryption key length must be between %d and %d", internal.MIN_ENCRYPTION_KEY_LENGTH, internal.MAX_ENCRYPTION_KEY_LENGTH))
} }
*password = internal.GenerateChars(*passwordLength) *encryptionKey = internal.GenerateChars(*encryptionKeyLength)
} }
if len(content) == 0 { if len(content) == 0 {
@ -173,13 +173,13 @@ func handleMain() int {
p.Language = *language p.Language = *language
} }
if *password != "" { if *encryptionKey != "" {
logger.Debug("validating password") logger.Debug("validating encryption key")
if err = internal.ValidatePassword(*password); err != nil { if err = internal.ValidateEncryptionKey(*encryptionKey); err != nil {
return internal.ReturnError(logger, "invalid password", nil) return internal.ReturnError(logger, "invalid encryption key", nil)
} }
logger.Debug("encrypting content") logger.Debug("encrypting content")
content, err = internal.Encrypt(content, *password) content, err = internal.Encrypt(content, *encryptionKey)
if err != nil { if err != nil {
return internal.ReturnError(logger, "could not encrypt note", err) return internal.ReturnError(logger, "could not encrypt note", err)
} }
@ -242,21 +242,21 @@ func handleMain() int {
logger.Debug("finding note location") logger.Debug("finding note location")
var location string var location string
noteURL := *url + "/" + jsonBody.ID noteURL := *url + "/" + jsonBody.ID
if *password != "" { if *encryptionKey != "" {
if *copier { if *copier {
location = fmt.Sprintf("copier -password %s %s", *password, noteURL) location = fmt.Sprintf("copier -encryption-key %s %s", *encryptionKey, noteURL)
} else { } else {
if *html { if *html {
location = fmt.Sprintf("%s/%s.html", noteURL, *password) location = fmt.Sprintf("%s/%s.html", noteURL, *encryptionKey)
} else { } else {
location = fmt.Sprintf("%s/%s", noteURL, *password) location = fmt.Sprintf("%s/%s", noteURL, *encryptionKey)
} }
} }
} else { } else {
if *html { if *html {
location = fmt.Sprintf("%s.html", noteURL) location = fmt.Sprintf("%s.html", noteURL)
} else { } else {
location = fmt.Sprintf("%s", noteURL) location = noteURL
} }
} }

View file

@ -19,7 +19,7 @@ The file format is **JSON**:
* **database_type** (string): Type of the database (default "sqlite", "postgres" also supported) * **database_type** (string): Type of the database (default "sqlite", "postgres" also supported)
* **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))
* **password_length** (int): Number of characters for generated passwords (default 16) * **encryption_key_length** (int): Number of characters for generated encryption key (default 16)
* **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)
@ -52,7 +52,7 @@ Create a note.
Body (JSON): Body (JSON):
* **content** (string): base64 encoded content (required) * **content** (string): base64 encoded content (required)
* **password** (string): use server-side encryption with this password * **encryption_key** (string): use server-side encryption with this encryption key
* **encrypted** (bool): true if the content has been encrypted by the client * **encrypted** (bool): true if the content has been encrypted by the client
* **expiration** (int): lifetime of the note in seconds (must be supported by the server) * **expiration** (int): lifetime of the note in seconds (must be supported by the server)
* **delete_after_read** (bool): delete the note after the first read * **delete_after_read** (bool): delete the note after the first read
@ -62,12 +62,12 @@ Response (JSON):
* **id** (string): ID of the note * **id** (string): ID of the note
### GET /\<id\>/\<password\> ### GET /\<id\>/\<encryptionKey\>
> [!WARNING] > [!WARNING]
> Potential password leak > Potential encryption key leak
Return content of a note encrypted by the given password. Return content of a note encrypted by the given encryption key.
### GET /\<id\> ### GET /\<id\>

View file

@ -70,7 +70,7 @@ func handleMain() int {
return internal.ReturnError(logger, "could not create server", err) return internal.ReturnError(logger, "could not create server", err)
} }
srv.SetPasswordLength(config.PasswordLength) srv.SetEncryptionKeyLength(config.EncryptionKeyLength)
if config.EnableMetrics { if config.EnableMetrics {
reg := prometheus.NewRegistry() reg := prometheus.NewRegistry()

View file

@ -11,6 +11,6 @@ copier -help
# Examples # Examples
``` ```
copier -password PASSWORD URL copier -encryption-key ENCRYPTION_KEY URL
copier -ask-password URL copier -ask-encryption-key URL
``` ```

View file

@ -9,8 +9,9 @@ import (
"os" "os"
"syscall" "syscall"
"git.riou.xyz/jriou/coller/internal"
"golang.org/x/term" "golang.org/x/term"
"git.riou.xyz/jriou/coller/internal"
) )
var ( var (
@ -28,8 +29,8 @@ func handleMain() int {
quiet := flag.Bool("quiet", false, "Log errors only") quiet := flag.Bool("quiet", false, "Log errors only")
verbose := flag.Bool("verbose", false, "Print more logs") verbose := flag.Bool("verbose", false, "Print more logs")
debug := flag.Bool("debug", false, "Print even more logs") debug := flag.Bool("debug", false, "Print even more logs")
password := flag.String("password", os.Getenv("COLLER_PASSWORD"), "Password to decrypt the note") encryptionKey := flag.String("encryption-key", os.Getenv("COLLER_ENCRYPTION_KEY"), "Key to decrypt the note")
askPassword := flag.Bool("ask-password", false, "Read password from input") askEncryptionKey := flag.Bool("ask-encryption-key", false, "Read encryption key from input")
fileName := flag.String("file", "", "Write content of the note to a file") fileName := flag.String("file", "", "Write content of the note to a file")
bearer := flag.String("bearer", os.Getenv("COLLER_BEARER"), "Bearer token") bearer := flag.String("bearer", os.Getenv("COLLER_BEARER"), "Bearer token")
askBearer := flag.Bool("ask-bearer", false, "Read bearer token from input") askBearer := flag.Bool("ask-bearer", false, "Read bearer token from input")
@ -60,13 +61,13 @@ func handleMain() int {
} }
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: level})) logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: level}))
if *askPassword { if *askEncryptionKey {
fmt.Print("Password: ") fmt.Print("Encryption key: ")
p, err := term.ReadPassword(int(syscall.Stdin)) p, err := term.ReadPassword(int(syscall.Stdin))
if err != nil { if err != nil {
return internal.ReturnError(logger, "could not read password", err) return internal.ReturnError(logger, "could not read encryption key", err)
} }
*password = string(p) *encryptionKey = string(p)
fmt.Print("\n") fmt.Print("\n")
} }
@ -102,11 +103,11 @@ func handleMain() int {
} }
var content []byte var content []byte
if *password != "" { if *encryptionKey != "" {
logger.Debug("decrypting note") logger.Debug("decrypting note")
content, err = internal.Decrypt(body, *password) content, err = internal.Decrypt(body, *encryptionKey)
if err != nil { if err != nil {
return internal.ReturnError(logger, "could not decrypt paste", err) return internal.ReturnError(logger, "could not decrypt note", err)
} }
} else { } else {
content = body content = body

View file

@ -19,21 +19,21 @@ const (
// NewCipher creates a cipher using XChaCha20-Poly1305 // NewCipher creates a cipher using XChaCha20-Poly1305
// https://pkg.go.dev/golang.org/x/crypto/chacha20poly1305 // https://pkg.go.dev/golang.org/x/crypto/chacha20poly1305
// A salt is required to derive the key from a password using argon // A salt is required to derive the key from an encryption key using argon
func NewCipher(password string, salt []byte) (cipher.AEAD, error) { func NewCipher(encryptionKey string, salt []byte) (cipher.AEAD, error) {
key := argon2.IDKey([]byte(password), salt, KeyTime, KeyMemory, KeyThreads, KeySize) key := argon2.IDKey([]byte(encryptionKey), salt, KeyTime, KeyMemory, KeyThreads, KeySize)
return chacha20poly1305.NewX(key) return chacha20poly1305.NewX(key)
} }
// Encrypt to encrypt a plaintext with a password // Encrypt to encrypt a plaintext with an encryption key
// Returns a byte slice with the generated salt, nonce and the ciphertext // Returns a byte slice with the generated salt, nonce and the ciphertext
func Encrypt(plaintext []byte, password string) (result []byte, err error) { func Encrypt(plaintext []byte, encryptionKey string) (result []byte, err error) {
salt := make([]byte, SaltSize) salt := make([]byte, SaltSize)
if n, err := rand.Read(salt); err != nil || n != SaltSize { if n, err := rand.Read(salt); err != nil || n != SaltSize {
return nil, err return nil, err
} }
aead, err := NewCipher(password, salt) aead, err := NewCipher(encryptionKey, salt)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -53,15 +53,15 @@ func Encrypt(plaintext []byte, password string) (result []byte, err error) {
return result, nil return result, nil
} }
// Decrypt to decrypt a ciphertext with a password // Decrypt to decrypt a ciphertext with a encryption key
// Returns the plaintext // Returns the plaintext
func Decrypt(ciphertext []byte, password string) ([]byte, error) { func Decrypt(ciphertext []byte, encryptionKey string) ([]byte, error) {
if len(ciphertext) < SaltSize { if len(ciphertext) < SaltSize {
return nil, fmt.Errorf("ciphertext is too short: cannot read salt") return nil, fmt.Errorf("ciphertext is too short: cannot read salt")
} }
salt := ciphertext[:SaltSize] salt := ciphertext[:SaltSize]
aead, err := NewCipher(password, salt) aead, err := NewCipher(encryptionKey, salt)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -6,10 +6,10 @@ import (
func TestEncryptAndDecrypt(t *testing.T) { func TestEncryptAndDecrypt(t *testing.T) {
plaintext := "test" plaintext := "test"
password := "test" encryptionKey := "test"
wrongPassword := password + "wrong" wrongEncryptionKey := encryptionKey + "wrong"
ciphertext, err := Encrypt([]byte(plaintext), password) ciphertext, err := Encrypt([]byte(plaintext), encryptionKey)
if err != nil { if err != nil {
t.Errorf("unexpected error when encrypting: %v", err) t.Errorf("unexpected error when encrypting: %v", err)
return return
@ -20,7 +20,7 @@ func TestEncryptAndDecrypt(t *testing.T) {
return return
} }
cleartext, err := Decrypt(ciphertext, password) cleartext, err := Decrypt(ciphertext, encryptionKey)
if err != nil { if err != nil {
t.Errorf("unexpected error when decrypting: %v", err) t.Errorf("unexpected error when decrypting: %v", err)
return return
@ -31,14 +31,14 @@ func TestEncryptAndDecrypt(t *testing.T) {
return return
} }
if password == wrongPassword { if encryptionKey == wrongEncryptionKey {
t.Errorf("passwords must be different") t.Errorf("encryption keys must be different")
return return
} }
_, err = Decrypt(ciphertext, wrongPassword) _, err = Decrypt(ciphertext, wrongEncryptionKey)
if err == nil { if err == nil {
t.Errorf("expected error when decrypting with a wrong password, got none") t.Errorf("expected error when decrypting with a wrong encryption key, got none")
return return
} }
} }

View file

@ -1,8 +1,8 @@
package internal package internal
const ( const (
RC_OK = 0 RC_OK = 0
RC_ERROR = 1 RC_ERROR = 1
MIN_PASSWORD_LENGTH = 16 MIN_ENCRYPTION_KEY_LENGTH = 16
MAX_PASSWORD_LENGTH = 256 MAX_ENCRYPTION_KEY_LENGTH = 256
) )

View file

@ -58,13 +58,13 @@ func GenerateChars(n int) string {
return string(b) return string(b)
} }
// Passwords must be URL compatible and strong enough // Encryption key must be URL compatible and strong enough
// Requiring only alphanumeric chars with a size between 16 and 256 // Requiring only alphanumeric chars with a size between 16 and 256
var passwordRegexp = regexp.MustCompile("^[a-zA-Z0-9]{16,256}$") var encryptionKeyRegexp = regexp.MustCompile("^[a-zA-Z0-9]{16,256}$")
func ValidatePassword(p string) error { func ValidateEncryptionKey(p string) error {
if !passwordRegexp.MatchString(p) { if !encryptionKeyRegexp.MatchString(p) {
return fmt.Errorf("password doesn't match '%s'", passwordRegexp) return fmt.Errorf("encryption key doesn't match '%s'", encryptionKeyRegexp)
} }
return nil return nil
} }

View file

@ -11,7 +11,7 @@ type Config struct {
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"`
PasswordLength int `json:"password_length"` EncryptionKeyLength int `json:"encryption_key_length"`
ExpirationInterval int `json:"expiration_interval"` ExpirationInterval int `json:"expiration_interval"`
ListenAddress string `json:"listen_address"` ListenAddress string `json:"listen_address"`
ListenPort int `json:"listen_port"` ListenPort int `json:"listen_port"`
@ -33,14 +33,14 @@ type Config struct {
func NewConfig() *Config { func NewConfig() *Config {
return &Config{ return &Config{
Title: "Coller", Title: "Coller",
DatabaseType: "sqlite", DatabaseType: "sqlite",
DatabaseDsn: "collerd.db", DatabaseDsn: "collerd.db",
NodeID: 1, NodeID: 1,
PasswordLength: 16, EncryptionKeyLength: 16,
ExpirationInterval: 60, // 1 minute ExpirationInterval: 60, // 1 minute
ListenAddress: "0.0.0.0", ListenAddress: "0.0.0.0",
ListenPort: 8080, ListenPort: 8080,
Expirations: []int{ Expirations: []int{
300, // 5 minutes 300, // 5 minutes
3600, // 1 hour 3600, // 1 hour
@ -92,8 +92,8 @@ func (c *Config) Check() error {
return fmt.Errorf("node id must be between 0 and 1023") return fmt.Errorf("node id must be between 0 and 1023")
} }
if c.PasswordLength < internal.MIN_PASSWORD_LENGTH || c.PasswordLength > internal.MAX_PASSWORD_LENGTH { if c.EncryptionKeyLength < internal.MIN_ENCRYPTION_KEY_LENGTH || c.EncryptionKeyLength > internal.MAX_ENCRYPTION_KEY_LENGTH {
return fmt.Errorf("password length must be between %d and %d", internal.MIN_PASSWORD_LENGTH, internal.MAX_PASSWORD_LENGTH) return fmt.Errorf("encryption key length must be between %d and %d", internal.MIN_ENCRYPTION_KEY_LENGTH, internal.MAX_ENCRYPTION_KEY_LENGTH)
} }
return nil return nil
} }

View file

@ -122,7 +122,7 @@ func (d *Database) Get(id string) (*Note, error) {
return nil, nil return nil, nil
} }
func (d *Database) Create(content []byte, password string, encrypted bool, expiration int, deleteAfterRead bool, language string) (note *Note, err error) { func (d *Database) Create(content []byte, encryptionKey string, encrypted bool, expiration int, deleteAfterRead bool, language string) (note *Note, err error) {
if expiration == 0 { if expiration == 0 {
expiration = d.expiration expiration = d.expiration
} }
@ -148,11 +148,11 @@ func (d *Database) Create(content []byte, password string, encrypted bool, expir
DeleteAfterRead: deleteAfterRead, DeleteAfterRead: deleteAfterRead,
Language: language, Language: language,
} }
if password != "" { if encryptionKey != "" {
if err = internal.ValidatePassword(password); err != nil { if err = internal.ValidateEncryptionKey(encryptionKey); err != nil {
return nil, err return nil, err
} }
note.Content, err = internal.Encrypt(note.Content, password) note.Content, err = internal.Encrypt(note.Content, encryptionKey)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -23,7 +23,7 @@ type CreateNoteHandler struct {
type CreateNotePayload struct { type CreateNotePayload struct {
Content string `json:"content"` Content string `json:"content"`
Password string `json:"password"` EncryptionKey string `json:"encryption_key"`
Encrypted bool `json:"encrypted"` Encrypted bool `json:"encrypted"`
Expiration int `json:"expiration"` Expiration int `json:"expiration"`
DeleteAfterRead bool `json:"delete_after_read"` DeleteAfterRead bool `json:"delete_after_read"`
@ -54,7 +54,7 @@ func (h *CreateNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return return
} }
note, err := h.db.Create(content, body.Password, body.Encrypted, body.Expiration, body.DeleteAfterRead, body.Language) note, err := h.db.Create(content, body.EncryptionKey, body.Encrypted, body.Expiration, body.DeleteAfterRead, body.Language)
if err != nil { if err != nil {
WriteError(w, "could not create note", err) WriteError(w, "could not create note", err)
return return
@ -99,7 +99,7 @@ func (h *GetProtectedNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
vars := mux.Vars(r) vars := mux.Vars(r)
id := vars["id"] id := vars["id"]
password := vars["password"] encryptionKey := vars["encryptionKey"]
note, err := h.db.Get(id) note, err := h.db.Get(id)
@ -111,8 +111,8 @@ func (h *GetProtectedNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque
return return
} }
if password != "" && note.Encrypted { if encryptionKey != "" && note.Encrypted {
note.Content, err = internal.Decrypt(note.Content, password) note.Content, err = internal.Decrypt(note.Content, encryptionKey)
if err != nil { if err != nil {
WriteError(w, "could not decrypt note", err) WriteError(w, "could not decrypt note", err)
return return

View file

@ -109,15 +109,15 @@ func (h *CreateNoteWithFormHandler) ServeHTTP(w http.ResponseWriter, r *http.Req
} }
h.logger.Debug("checking inputs") h.logger.Debug("checking inputs")
noPassword := r.FormValue("no-password") noEncryption := r.FormValue("no-encryption")
password := r.FormValue("password") encryptionKey := r.FormValue("encryption-key")
expiration := r.FormValue("expiration") expiration := r.FormValue("expiration")
deleteAfterRead := r.FormValue("delete-after-read") deleteAfterRead := r.FormValue("delete-after-read")
language := r.FormValue("language") language := r.FormValue("language")
if password == "" && noPassword == "" { if encryptionKey == "" && noEncryption == "" {
h.logger.Debug("generating password") h.logger.Debug("generating encryption key")
password = internal.GenerateChars(passwordLength) encryptionKey = internal.GenerateChars(encryptionKeyLength)
} }
h.logger.Debug("computing expiration") h.logger.Debug("computing expiration")
@ -129,7 +129,7 @@ func (h *CreateNoteWithFormHandler) ServeHTTP(w http.ResponseWriter, r *http.Req
} }
h.logger.Debug("saving note to the database") h.logger.Debug("saving note to the database")
note, err := h.db.Create(content, password, password != "", expirationInt, deleteAfterRead != "", language) note, err := h.db.Create(content, encryptionKey, encryptionKey != "", expirationInt, deleteAfterRead != "", language)
if err != nil { if err != nil {
h.PageData.Err = err h.PageData.Err = err
h.Templates.ExecuteTemplate(w, templateName, h.PageData) h.Templates.ExecuteTemplate(w, templateName, h.PageData)
@ -143,8 +143,8 @@ func (h *CreateNoteWithFormHandler) ServeHTTP(w http.ResponseWriter, r *http.Req
} }
h.PageData.URL = fmt.Sprintf("%s%s/%s", scheme, r.Host, note.ID) h.PageData.URL = fmt.Sprintf("%s%s/%s", scheme, r.Host, note.ID)
if password != "" { if encryptionKey != "" {
h.PageData.URL += "/" + password h.PageData.URL += "/" + encryptionKey
} }
h.logger.Debug("rendering page") h.logger.Debug("rendering page")
@ -197,7 +197,7 @@ func (h *GetProtectedWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
vars := mux.Vars(r) vars := mux.Vars(r)
id := vars["id"] id := vars["id"]
password := vars["password"] encryptionKey := vars["encryptionKey"]
note, err := h.db.Get(id) note, err := h.db.Get(id)
@ -213,8 +213,8 @@ func (h *GetProtectedWebNoteHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
return return
} }
if password != "" && note.Encrypted { if encryptionKey != "" && note.Encrypted {
note.Content, err = internal.Decrypt(note.Content, password) note.Content, err = internal.Decrypt(note.Content, encryptionKey)
if err != nil { if err != nil {
h.PageData.Err = fmt.Errorf("could not decrypt note: %v", err) h.PageData.Err = fmt.Errorf("could not decrypt note: %v", err)
h.Templates.ExecuteTemplate(w, templateName, h.PageData) h.Templates.ExecuteTemplate(w, templateName, h.PageData)

View file

@ -16,10 +16,10 @@ import (
) )
var ( var (
passwordLength = internal.MIN_PASSWORD_LENGTH encryptionKeyLength = internal.MIN_ENCRYPTION_KEY_LENGTH
supportedOSes = []string{"linux", "darwin"} supportedOSes = []string{"linux", "darwin"}
supportedArches = []string{"amd64", "arm64"} supportedArches = []string{"amd64", "arm64"}
supportedClients = []string{"coller", "copier"} supportedClients = []string{"coller", "copier"}
) )
type Server struct { type Server struct {
@ -41,8 +41,8 @@ func NewServer(logger *slog.Logger, db *Database, config *Config, version string
}, nil }, nil
} }
func (s *Server) SetPasswordLength(length int) { func (s *Server) SetEncryptionKeyLength(length int) {
passwordLength = length encryptionKeyLength = length
} }
func (s *Server) SetMetrics(metrics *Metrics) { func (s *Server) SetMetrics(metrics *Metrics) {
@ -100,7 +100,7 @@ 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") r.Path("/api/note").Handler(&CreateNoteHandler{logger: s.logger, db: s.db, maxUploadSize: s.config.MaxUploadSize}).Methods("POST")
r.Path("/{id:[a-zA-Z0-9]+}/{password:[a-zA-Z0-9]+}").Handler(&GetProtectedNoteHandler{logger: s.logger, db: s.db}).Methods("GET") r.Path("/{id:[a-zA-Z0-9]+}/{encryptionKey:[a-zA-Z0-9]+}").Handler(&GetProtectedNoteHandler{logger: s.logger, db: s.db}).Methods("GET")
r.Path("/{id:[a-zA-Z0-9]+}").Handler(&GetNoteHandler{logger: s.logger, db: s.db}).Methods("GET") r.Path("/{id:[a-zA-Z0-9]+}").Handler(&GetNoteHandler{logger: s.logger, db: s.db}).Methods("GET")
// Web pages // Web pages
@ -150,7 +150,7 @@ func (s *Server) Start() error {
logger: s.logger, logger: s.logger,
db: s.db, db: s.db,
} }
r.Path("/{id:[a-zA-Z0-9]+}/{password:[a-zA-Z0-9]+}.html").Handler(protectedWebNoteHandler).Methods("GET") r.Path("/{id:[a-zA-Z0-9]+}/{encryptionKey:[a-zA-Z0-9]+}.html").Handler(protectedWebNoteHandler).Methods("GET")
webNoteHandler := &GetWebNoteHandler{ webNoteHandler := &GetWebNoteHandler{
Templates: templates, Templates: templates,

View file

@ -14,17 +14,17 @@
<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">
<div class="col-1"> <div class="col-1">
<label class="col-form-label col-form-label-sm" for="password">Password</label> <label class="col-form-label col-form-label-sm" for="encryption-key">Encryption key</label>
</div> </div>
<div class="col"> <div class="col">
<input type="password" pattern="^[a-zA-Z0-9]{16,256}$" <input type="password" pattern="^[a-zA-Z0-9]{16,256}$"
title="Letters and numbers with length from 16 to 256" class="form-control" id="password" title="Letters and numbers with length from 16 to 256" class="form-control" id="encryption-key"
name="password"> name="encryption-key">
</div> </div>
<div class="col-1"> <div class="col-1">
<input type="checkbox" class="form-check-input" for="no-password" id="no-password" <input type="checkbox" class="form-check-input" for="no-encryption-key" id="no-encryption-key"
value="no-password" name="no-password"> value="no-encryption-key" name="no-encryption-key">
<label class="col-form-label col-form-label-sm" for="no-password">No password</label> <label class="col-form-label col-form-label-sm" for="no-encryption-key">No encryption</label>
</div> </div>
<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"