diff --git a/README.md b/README.md index 00f988054..0bf7e8ab0 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,42 @@ Access Memos at `http://localhost:5230` and complete the initial setup. **Pro Tip**: The data directory stores all your notes, uploads, and settings. Include it in your backup strategy! +### 🔒 Database Encryption (for SQLite) + +For enhanced security, Memos supports transparent, full-database encryption for SQLite using **SQLCipher**. This "Encryption at Rest" feature protects your database file even if your server's file system is compromised. + +> [!IMPORTANT] +> This is **not** End-to-End Encryption (E2E). The Memos server holds the key in memory to process data. It protects the database file on the disk, not data from an attacker who has compromised the running application. + +Enabling this feature is a two-step process: building a special version of Memos and providing a key at runtime. + +#### Using Docker (Recommended) + +1. **Build the SQLCipher-enabled image:** + ```bash + docker build \ + --build-arg CGO_ENABLED=1 \ + --build-arg MEMOS_BUILD_TAGS="memos_sqlcipher libsqlite3 sqlite_omit_load_extension" \ + -t memos-sqlcipher \ + -f scripts/Dockerfile . + ``` + +2. **Run the container with the encryption key:** + Provide your secret key via the `MEMOS_SQLITE_ENCRYPTION_KEY` environment variable. + ```bash + docker run -d \ + --name memos \ + -p 5230:5230 \ + -v ~/.memos:/var/opt/memos \ + -e MEMOS_SQLITE_ENCRYPTION_KEY="your-super-secret-key" \ + memos-sqlcipher + ``` + +> [!WARNING] +> **Key Management is Your Responsibility.** If you lose your encryption key, your data is **permanently unrecoverable**. Back up your key in a secure location like a password manager. + +For detailed instructions, including how to encrypt an existing database, please see our full documentation on **[Database Encryption](https://www.usememos.com/docs/advanced-settings/database-encryption)**. + ## Sponsors Memos is made possible by the generous support of our sponsors. Their contributions help ensure the project's continued development, maintenance, and growth. diff --git a/bin/memos/main.go b/bin/memos/main.go index 48dad202d..a5eabd3d2 100644 --- a/bin/memos/main.go +++ b/bin/memos/main.go @@ -25,15 +25,16 @@ var ( Short: `An open source, lightweight note-taking service. Easily capture and share your great thoughts.`, Run: func(_ *cobra.Command, _ []string) { instanceProfile := &profile.Profile{ - Mode: viper.GetString("mode"), - Addr: viper.GetString("addr"), - Port: viper.GetInt("port"), - UNIXSock: viper.GetString("unix-sock"), - Data: viper.GetString("data"), - Driver: viper.GetString("driver"), - DSN: viper.GetString("dsn"), - InstanceURL: viper.GetString("instance-url"), - Version: version.GetCurrentVersion(viper.GetString("mode")), + Mode: viper.GetString("mode"), + Addr: viper.GetString("addr"), + Port: viper.GetInt("port"), + UNIXSock: viper.GetString("unix-sock"), + Data: viper.GetString("data"), + Driver: viper.GetString("driver"), + DSN: viper.GetString("dsn"), + SQLiteEncryptionKey: viper.GetString("sqlite-encryption-key"), + InstanceURL: viper.GetString("instance-url"), + Version: version.GetCurrentVersion(viper.GetString("mode")), } if err := instanceProfile.Validate(); err != nil { panic(err) @@ -100,6 +101,7 @@ func init() { rootCmd.PersistentFlags().String("data", "", "data directory") rootCmd.PersistentFlags().String("driver", "sqlite", "database driver") rootCmd.PersistentFlags().String("dsn", "", "database source name(aka. DSN)") + rootCmd.PersistentFlags().String("sqlite-encryption-key", "", "SQLCipher key used to unlock the SQLite database (requires binary built with memos_sqlcipher)") rootCmd.PersistentFlags().String("instance-url", "", "the url of your memos instance") if err := viper.BindPFlag("mode", rootCmd.PersistentFlags().Lookup("mode")); err != nil { @@ -123,6 +125,9 @@ func init() { if err := viper.BindPFlag("dsn", rootCmd.PersistentFlags().Lookup("dsn")); err != nil { panic(err) } + if err := viper.BindPFlag("sqlite-encryption-key", rootCmd.PersistentFlags().Lookup("sqlite-encryption-key")); err != nil { + panic(err) + } if err := viper.BindPFlag("instance-url", rootCmd.PersistentFlags().Lookup("instance-url")); err != nil { panic(err) } @@ -132,6 +137,9 @@ func init() { if err := viper.BindEnv("instance-url", "MEMOS_INSTANCE_URL"); err != nil { panic(err) } + if err := viper.BindEnv("sqlite-encryption-key", "MEMOS_SQLITE_ENCRYPTION_KEY"); err != nil { + panic(err) + } } func printGreetings(profile *profile.Profile) { diff --git a/go.mod b/go.mod index 25b3e0dbe..06c83442e 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/labstack/echo/v4 v4.13.4 github.com/lib/pq v1.10.9 github.com/lithammer/shortuuid/v4 v4.2.0 + github.com/mattn/go-sqlite3 v1.14.32 github.com/pkg/errors v0.9.1 github.com/spf13/cobra v1.10.1 github.com/spf13/viper v1.20.1 diff --git a/go.sum b/go.sum index b831664f6..7d1ff8bf3 100644 --- a/go.sum +++ b/go.sum @@ -285,6 +285,8 @@ github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Ky github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= +github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= diff --git a/internal/profile/profile.go b/internal/profile/profile.go index 8d551d669..72727eb67 100644 --- a/internal/profile/profile.go +++ b/internal/profile/profile.go @@ -28,6 +28,8 @@ type Profile struct { // Driver is the database driver // sqlite, mysql Driver string + // SQLiteEncryptionKey unlocks SQLCipher-protected SQLite databases when provided. + SQLiteEncryptionKey string // Version is the current version of server Version string // InstanceURL is the url of your memos instance. @@ -88,5 +90,9 @@ func (p *Profile) Validate() error { p.DSN = filepath.Join(dataDir, dbFile) } + if p.SQLiteEncryptionKey != "" && p.Driver != "sqlite" { + return errors.New("sqlite encryption key is only supported when using the sqlite driver") + } + return nil } diff --git a/scripts/Dockerfile b/scripts/Dockerfile index 38456b37e..469970016 100644 --- a/scripts/Dockerfile +++ b/scripts/Dockerfile @@ -1,4 +1,12 @@ FROM golang:1.25-alpine AS backend +ARG MEMOS_BUILD_TAGS="" +ARG CGO_ENABLED=0 +ARG CGO_CFLAGS="" +ARG CGO_LDFLAGS="" +ENV CGO_ENABLED=${CGO_ENABLED} +ENV MEMOS_BUILD_TAGS=${MEMOS_BUILD_TAGS} +ENV CGO_CFLAGS=${CGO_CFLAGS} +ENV CGO_LDFLAGS=${CGO_LDFLAGS} WORKDIR /backend-build COPY go.mod go.sum ./ RUN go mod download @@ -7,13 +15,50 @@ COPY . . # Refer to `pnpm release` in package.json for the build command. RUN --mount=type=cache,target=/go/pkg/mod \ --mount=type=cache,target=/root/.cache/go-build \ - go build -ldflags="-s -w" -o memos ./bin/memos/main.go + /bin/sh -eux <<'EOF' +if [ "${CGO_ENABLED}" = "1" ]; then + apk add --no-cache --virtual .build-deps build-base pkgconf + if printf "%s" "${MEMOS_BUILD_TAGS}" | grep -q "memos_sqlcipher"; then + apk add --no-cache --virtual .sqlcipher-build sqlcipher-dev + SQLCIPHER_CFLAGS="$(pkg-config --cflags sqlcipher)" + SQLCIPHER_LDFLAGS="$(pkg-config --libs sqlcipher)" + if [ ! -e /usr/lib/libsqlite3.so ]; then + ln -s /usr/lib/libsqlcipher.so /usr/lib/libsqlite3.so + fi + if [ -z "${CGO_CFLAGS}" ]; then + export CGO_CFLAGS="${SQLCIPHER_CFLAGS} -DSQLITE_HAS_CODEC" + else + export CGO_CFLAGS="${CGO_CFLAGS} ${SQLCIPHER_CFLAGS} -DSQLITE_HAS_CODEC" + fi + if [ -z "${CGO_LDFLAGS}" ]; then + export CGO_LDFLAGS="${SQLCIPHER_LDFLAGS}" + else + export CGO_LDFLAGS="${CGO_LDFLAGS} ${SQLCIPHER_LDFLAGS}" + fi + fi +fi + +go build -ldflags="-s -w" -tags="${MEMOS_BUILD_TAGS}" -o memos ./bin/memos/main.go + +if [ "${CGO_ENABLED}" = "1" ]; then + if apk info -e .sqlcipher-build >/dev/null 2>&1; then + apk del .sqlcipher-build + fi + apk del .build-deps +fi +EOF # Make workspace with above generated files. FROM alpine:latest AS monolithic +ARG MEMOS_BUILD_TAGS="" WORKDIR /usr/local/memos RUN apk add --no-cache tzdata +RUN if printf "%s" "$MEMOS_BUILD_TAGS" | grep -q "memos_sqlcipher"; then \ + apk add --no-cache sqlcipher sqlcipher-libs && \ + if [ -e /usr/lib/libsqlcipher.so ]; then ln -sf /usr/lib/libsqlcipher.so /usr/lib/libsqlite3.so; fi && \ + if [ -e /usr/lib/libsqlcipher.so.0 ]; then ln -sf /usr/lib/libsqlcipher.so.0 /usr/lib/libsqlcipher.so; fi; \ + fi ENV TZ="UTC" COPY --from=backend /backend-build/memos /usr/local/memos/ diff --git a/server/router/frontend/dist/index.html b/server/router/frontend/dist/index.html index a612ed1f7..f5f986278 100644 --- a/server/router/frontend/dist/index.html +++ b/server/router/frontend/dist/index.html @@ -1,11 +1,22 @@ - +
- + + + + +