├── .dockerignore ├── .gitattributes ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── app └── service.go ├── config └── config.go ├── crypt ├── crypt_test.go ├── keys.go └── rsa │ ├── mock.go │ └── rsa.go ├── deploy.sh ├── go.mod ├── go.sum ├── handler ├── handler_test.go ├── helpers.go ├── message.go ├── static.go └── webModels │ └── main.go ├── i18n ├── entries.go ├── i18n.go └── i18n_test.go ├── logs └── main.go ├── main.go ├── repository ├── counter.go ├── firestore.go ├── mock │ └── mock.go └── model │ ├── key.go │ └── message.go ├── revive.toml ├── variables.json └── web ├── .gitignore ├── .npmrc ├── README.md ├── jsconfig.json ├── migrate.txt ├── package-lock.json ├── package.json ├── src ├── app.d.ts ├── app.html ├── app.scss ├── lib │ └── index.js ├── routes │ ├── +layout.js │ ├── +layout.svelte │ ├── +page.js │ ├── +page.svelte │ ├── Commons.js │ ├── Decrypt.svelte │ ├── Encrypt.svelte │ ├── ShowLink.svelte │ └── ShowMessage.svelte └── variables.scss ├── static ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── robots.txt └── site.webmanifest ├── svelte.config.js └── vite.config.js /.dockerignore: -------------------------------------------------------------------------------- 1 | */node_modules 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | static/* linguist-vendored -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/intellij+all 3 | # Edit at https://www.gitignore.io/?templates=intellij+all 4 | 5 | ### Intellij+all ### 6 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 7 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 8 | 9 | # User-specific stuff 10 | .idea/**/workspace.xml 11 | .idea/**/tasks.xml 12 | .idea/**/usage.statistics.xml 13 | .idea/**/dictionaries 14 | .idea/**/shelf 15 | 16 | # Generated files 17 | .idea/**/contentModel.xml 18 | 19 | # Sensitive or high-churn files 20 | .idea/**/dataSources/ 21 | .idea/**/dataSources.ids 22 | .idea/**/dataSources.local.xml 23 | .idea/**/sqlDataSources.xml 24 | .idea/**/dynamic.xml 25 | .idea/**/uiDesigner.xml 26 | .idea/**/dbnavigator.xml 27 | 28 | # Gradle 29 | .idea/**/gradle.xml 30 | .idea/**/libraries 31 | 32 | # Gradle and Maven with auto-import 33 | # When using Gradle or Maven with auto-import, you should exclude module files, 34 | # since they will be recreated, and may cause churn. Uncomment if using 35 | # auto-import. 36 | # .idea/modules.xml 37 | # .idea/*.iml 38 | # .idea/modules 39 | # *.iml 40 | # *.ipr 41 | 42 | # CMake 43 | cmake-build-*/ 44 | 45 | # Mongo Explorer plugin 46 | .idea/**/mongoSettings.xml 47 | 48 | # File-based project format 49 | *.iws 50 | 51 | # IntelliJ 52 | out/ 53 | 54 | # mpeltonen/sbt-idea plugin 55 | .idea_modules/ 56 | 57 | # JIRA plugin 58 | atlassian-ide-plugin.xml 59 | 60 | # Cursive Clojure plugin 61 | .idea/replstate.xml 62 | 63 | # Crashlytics plugin (for Android Studio and IntelliJ) 64 | com_crashlytics_export_strings.xml 65 | crashlytics.properties 66 | crashlytics-build.properties 67 | fabric.properties 68 | 69 | # Editor-based Rest Client 70 | .idea/httpRequests 71 | 72 | # Android studio 3.1+ serialized cache file 73 | .idea/caches/build_file_checksums.ser 74 | 75 | ### Intellij+all Patch ### 76 | # Ignores the whole .idea folder and all .iml files 77 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 78 | 79 | .idea/ 80 | 81 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 82 | 83 | *.iml 84 | modules.xml 85 | .idea/misc.xml 86 | *.ipr 87 | 88 | # Sonarlint plugin 89 | .idea/sonarlint 90 | 91 | # End of https://www.gitignore.io/api/intellij+all 92 | 93 | web2 -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:23-alpine AS nodeBuilder 2 | WORKDIR /app 3 | COPY ./web ./ 4 | ENV PATH /app/node_modules/.bin:$PATH 5 | RUN npm install 6 | RUN npm run build 7 | 8 | FROM golang:1.24 as goBuilder 9 | RUN useradd -u 10001 -d /app scratchuser 10 | WORKDIR /app 11 | COPY go.* ./ 12 | RUN go mod download 13 | COPY . ./ 14 | COPY --from=nodeBuilder /app/build /app/web/build/ 15 | RUN CGO_ENABLED=0 GOOS=linux go build -mod=readonly -ldflags "-s -w" -v -o server 16 | 17 | FROM scratch 18 | COPY --from=goBuilder /app/server /server 19 | COPY --from=goBuilder /etc/passwd /etc/passwd 20 | USER scratchuser 21 | CMD ["/server"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Tomasz Bluszko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PKGS := $(shell go list ./...) 2 | 3 | all: codequality test security build 4 | 5 | test: 6 | @echo ">> TEST, \"full-mode\": race detector on" 7 | @$(foreach pkg, $(PKGS),\ 8 | echo -n " ";\ 9 | go test -run '(Test|Example)' -race $(pkg) || exit 1;\ 10 | ) 11 | 12 | codequality: 13 | @echo ">> CODE QUALITY" 14 | 15 | @echo -n " GOLANGCI-LINTERS \n" 16 | @golangci-lint -v run ./... 17 | @$(call ok) 18 | 19 | @echo -n " REVIVE" 20 | @revive -config revive.toml -formatter friendly -exclude vendor/... ./... 21 | @$(call ok) 22 | 23 | security: 24 | @echo ">> CHECKING FOR INSECURE DEPENDENCIES USING GOVULNCHECK" 25 | @govulncheck ./... 26 | @echo ">> CHECKING FOR INSECURE DEPENDENCIES USING NANCY" 27 | @go list -json -deps | nancy sleuth 28 | @$(call ok) 29 | 30 | build: 31 | @echo -n ">> BUILD" 32 | @npm install --prefix web 33 | @npm run build --prefix web 34 | @go build $(PKGS) 35 | @$(call ok) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Tool uses various method of encryption to ensure maximum privacy (Curve25519, XSalsa20, RSA, Scrypt key derivation function). 2 | 3 | Message is encrypted with NaCl Secret Box using https://tweetnacl.js.org/, JavaScript implementation of 4 | Networking and Cryptography library (NaCl https://nacl.cr.yp.to/). Nonce used for Secret Box is used to generate 5 | link anchor which is used then to retrieve the message. Nonce is necessary to decrypt the message, it is not 6 | saved anywhere else so only user using the link can decode the message. To increase security one can use a password. 7 | This password will be used to generate ephemeral security key. 8 | 9 | Encrypted message with secret key is sealed again using asymmetric algorithm NaCl Box and stored in Database. 10 | 11 | All keys and nonces on browser side are unique for every action. NaCl Box server keys are generated while application 12 | started for the first time and are encrypted at rest using RSASSA-PSS 3072 bit key with a SHA-256 digest. 13 | RSA encryption/decryption keys use Cloud KMS module. 14 | 15 | Service runs in Google Cloud RUN infrastructure and is available on https://securenote.io/. 16 | 17 | Try it locally: 18 | ``` 19 | npm install --prefix web 20 | npm run build --prefix web 21 | go run . 22 | 23 | and go http://localhost:3000/ 24 | -------------------------------------------------------------------------------- /app/service.go: -------------------------------------------------------------------------------- 1 | package app 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log/slog" 7 | "net/url" 8 | "time" 9 | 10 | "obliviate/config" 11 | "obliviate/crypt" 12 | "obliviate/handler/webModels" 13 | "obliviate/repository" 14 | "obliviate/repository/model" 15 | ) 16 | 17 | type App struct { 18 | Config *config.Configuration 19 | keys *crypt.Keys 20 | db repository.DataBase 21 | } 22 | 23 | func NewApp(db repository.DataBase, config *config.Configuration, keys *crypt.Keys) *App { 24 | app := App{ 25 | Config: config, 26 | keys: keys, 27 | db: db, 28 | } 29 | return &app 30 | } 31 | 32 | func (s *App) ProcessSave(ctx context.Context, request webModels.SaveRequest) error { 33 | hashEncoded := url.PathEscape(request.Hash) 34 | countryCode := ctx.Value(config.CountryCode).(string) 35 | 36 | messageDataModel := model.NewMessage(hashEncoded, request.Message, time.Now().Add(s.Config.DefaultDurationTime), 37 | request.TransmissionNonce, request.PublicKey, request.Time, request.CostFactor, countryCode) 38 | 39 | err := s.db.SaveMessage(ctx, messageDataModel) 40 | if err != nil { 41 | return fmt.Errorf("cannot save message, err: %v", err) 42 | } 43 | 44 | go func() { 45 | s.db.IncreaseCounter(context.Background()) 46 | }() 47 | 48 | return nil 49 | } 50 | 51 | func (s *App) ProcessRead(ctx context.Context, request webModels.ReadRequest) ([]byte, int, error) { 52 | hashEncoded := url.PathEscape(request.Hash) 53 | 54 | data, err := s.db.GetMessage(ctx, hashEncoded) 55 | if err != nil { 56 | return nil, 0, fmt.Errorf("error in GetMessage, err: %v", err) 57 | } 58 | if data.Txt == nil { 59 | return nil, 0, nil 60 | } 61 | var senderPublicKey [32]byte 62 | copy(senderPublicKey[:], data.PublicKey) 63 | 64 | var senderNonce [24]byte 65 | copy(senderNonce[:], data.Nonce) 66 | 67 | decrypted, err := s.keys.BoxOpen(data.Txt, &senderPublicKey, &senderNonce) 68 | if err != nil { 69 | return nil, 0, fmt.Errorf("cannot open box, err: %v", err) 70 | } 71 | 72 | var recipientPublicKey [32]byte 73 | copy(recipientPublicKey[:], request.PublicKey) 74 | 75 | encrypted, err := s.keys.BoxSeal(decrypted, &recipientPublicKey) 76 | if err != nil { 77 | return nil, 0, fmt.Errorf("cannot seal message, err: %v", err) 78 | } 79 | 80 | if !request.Password { 81 | // delete only when password is not required 82 | go func() { 83 | s.db.DeleteMessage(context.Background(), hashEncoded) 84 | }() 85 | } 86 | 87 | return encrypted, data.CostFactor, nil 88 | } 89 | 90 | func (s *App) ProcessDelete(ctx context.Context, hash string) { 91 | hashEncoded := url.PathEscape(hash) 92 | s.db.DeleteMessage(ctx, hashEncoded) 93 | } 94 | 95 | func (s *App) ProcessDeleteExpired(ctx context.Context) error { 96 | if err := s.db.DeleteBeforeNow(ctx); err != nil { 97 | return fmt.Errorf("delete expired error: %v", err) 98 | } 99 | slog.InfoContext(ctx, "Delete expired, done") 100 | return nil 101 | } 102 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "io/fs" 5 | "time" 6 | ) 7 | 8 | type contextKey string 9 | 10 | var ( 11 | CountryCode = contextKey("country-code") 12 | AcceptLanguage = contextKey("accept-language") 13 | ) 14 | 15 | type Configuration struct { 16 | DefaultDurationTime time.Duration 17 | ProdEnv bool 18 | MasterKey string 19 | KmsCredentialFile string 20 | FirestoreCredentialFile string 21 | StaticFilesLocation string 22 | EmbededStaticFiles fs.FS 23 | } 24 | -------------------------------------------------------------------------------- /crypt/crypt_test.go: -------------------------------------------------------------------------------- 1 | package crypt 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | 10 | "obliviate/config" 11 | "obliviate/crypt/rsa" 12 | "obliviate/repository" 13 | "obliviate/repository/mock" 14 | ) 15 | 16 | var conf *config.Configuration 17 | var db repository.DataBase 18 | 19 | func init() { 20 | conf = &config.Configuration{ 21 | DefaultDurationTime: time.Hour * 24 * 7, 22 | ProdEnv: os.Getenv("ENV") == "PROD", 23 | MasterKey: os.Getenv("HSM_MASTER_KEY"), 24 | KmsCredentialFile: os.Getenv("KMS_CREDENTIAL_FILE"), 25 | FirestoreCredentialFile: os.Getenv("FIRESTORE_CREDENTIAL_FILE"), 26 | } 27 | // conf.Db = repository.Connect(context.Background(), "test") 28 | db = mock.StorageMock() 29 | } 30 | 31 | func TestKeysGenerationAndStorage(t *testing.T) { 32 | 33 | rsa := rsa.NewMockAlgorithm() 34 | // rsa := rsa.NewAlgorithm() 35 | 36 | keys, err := NewKeys(db, conf, rsa, true) 37 | assert.NoError(t, err, "should not be error") 38 | 39 | pubKey := keys.PublicKeyEncoded 40 | 41 | var priv [32]byte 42 | //nolint:gosimple 43 | var pub [32]byte 44 | pub = *keys.PublicKey 45 | priv = *keys.PrivateKey 46 | 47 | keys, err = NewKeys(db, conf, rsa, true) 48 | assert.NoError(t, err, "should not be error") 49 | 50 | assert.Equal(t, pubKey, keys.PublicKeyEncoded, "private keys should be the same") 51 | assert.Equal(t, priv, *keys.PrivateKey, "private keys should be the same") 52 | assert.Equal(t, pub, *keys.PublicKey, "public keys should be the same") 53 | 54 | } 55 | -------------------------------------------------------------------------------- /crypt/keys.go: -------------------------------------------------------------------------------- 1 | package crypt 2 | 3 | import ( 4 | "context" 5 | "crypto/rand" 6 | "encoding/base64" 7 | "fmt" 8 | "io" 9 | "log/slog" 10 | 11 | "golang.org/x/crypto/nacl/box" 12 | 13 | "obliviate/config" 14 | "obliviate/crypt/rsa" 15 | "obliviate/repository" 16 | ) 17 | 18 | type Keys struct { 19 | PublicKey *[32]byte 20 | PrivateKey *[32]byte 21 | PublicKeyEncoded string 22 | } 23 | 24 | func NewKeys(db repository.DataBase, conf *config.Configuration, algorithm rsa.EncryptionOnRest, expectKeys bool) (*Keys, error) { 25 | 26 | k := Keys{} 27 | 28 | encrypted, err := db.GetEncryptedKeys(context.Background()) 29 | if err != nil { 30 | return nil, fmt.Errorf("error retreaving keys from DB: %v", err) 31 | } 32 | 33 | if encrypted != nil { 34 | // decrypt Keys 35 | decrypted, err := algorithm.Decrypt(conf, encrypted) 36 | if err != nil { 37 | return nil, fmt.Errorf("error decryting keys: %v", err) 38 | } 39 | k.PublicKey = new([32]byte) 40 | k.PrivateKey = new([32]byte) 41 | 42 | copy(k.PublicKey[:], decrypted[:32]) 43 | copy(k.PrivateKey[:], decrypted[32:]) 44 | 45 | slog.Debug("encryption keys fetched and decrypted by master key") 46 | 47 | } else { 48 | if conf.ProdEnv && expectKeys { // prevent to overwrite the keys 49 | slog.Error("Keys expected") 50 | panic("Keys expected") 51 | } 52 | 53 | // generate Keys 54 | k.PublicKey, k.PrivateKey, err = box.GenerateKey(rand.Reader) 55 | if err != nil { 56 | return nil, fmt.Errorf("error generating keys: %v", err) 57 | } 58 | both := append(k.PublicKey[:], k.PrivateKey[:]...) 59 | 60 | // encrypt Keys 61 | encrypted, err = algorithm.Encrypt(conf, both) 62 | if err != nil { 63 | return nil, fmt.Errorf("error encrypting keys: %v", err) 64 | } 65 | 66 | // repository crypted Keys 67 | err = db.SaveEncryptedKeys(context.Background(), encrypted) 68 | if err != nil { 69 | return nil, fmt.Errorf("error storing keys into DB: %v", err) 70 | } 71 | 72 | slog.Debug("encryption keys generated, encrypted by master key, stored in DB") 73 | } 74 | 75 | k.PublicKeyEncoded = base64.StdEncoding.EncodeToString(k.PublicKey[:]) 76 | slog.Debug("encryption keys are ready") 77 | 78 | return &k, nil 79 | } 80 | 81 | func (keys *Keys) BoxOpen(encrypted []byte, senderPublicKey *[32]byte, decryptNonce *[24]byte) ([]byte, error) { 82 | 83 | decrypted, ok := box.Open(nil, encrypted, decryptNonce, senderPublicKey, keys.PrivateKey) 84 | if !ok { 85 | return nil, fmt.Errorf("cannot make box open") 86 | } 87 | return decrypted, nil 88 | } 89 | 90 | func (keys *Keys) BoxSeal(msg []byte, recipientPublicKey *[32]byte) ([]byte, error) { 91 | 92 | var nonce [24]byte 93 | var err error 94 | if nonce, err = keys.GenerateNonce(); err != nil { 95 | return nil, err 96 | } 97 | 98 | encrypted := box.Seal(nonce[:], msg, &nonce, recipientPublicKey, keys.PrivateKey) 99 | // nonce is already included in first 24 bytes of encrypted message 100 | return encrypted, nil 101 | } 102 | 103 | func (keys *Keys) GenerateNonce() ([24]byte, error) { 104 | var nonce [24]byte 105 | if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil { 106 | return nonce, fmt.Errorf("cannot generate nounce, err: %w", err) 107 | } 108 | return nonce, nil 109 | } 110 | -------------------------------------------------------------------------------- /crypt/rsa/mock.go: -------------------------------------------------------------------------------- 1 | package rsa 2 | 3 | import ( 4 | "obliviate/config" 5 | ) 6 | 7 | type mock struct { 8 | plaintext []byte 9 | } 10 | 11 | func NewMockAlgorithm() *mock { 12 | return &mock{} 13 | } 14 | 15 | func (m *mock) Encrypt(conf *config.Configuration, plaintext []byte) ([]byte, error) { 16 | m.plaintext = plaintext 17 | return []byte("38290823908 32908329083290382903890389032890238903829308290382903890283092803829083098" + 18 | "sdfdsf dsfds jfidso fjdsiofdsuifduifhdsuifhdsu ifdsuifhsui89ys9u uhuifhikhsuishafidskhdo sads " + 19 | "sdfdsf dsfds jfidso fjdsiofdsuifduifhdsuifhdsu ifdsuifhsui89ys9u uhuifhikhsuishafidskhdo sads " + 20 | "sdfdsf dsfds jfidso fjdsiofdsuifduifhdsuifhdsu ifdsuifhsui89ys9u uhuifhikhsuishafidskhdo sads " + 21 | "sdfdsf dsfds jfidso fjdsiofdsuifduifhdsuifhdsu ifdsuifhsui89ys9u uhuifhikhsuishafidskhdo sads " + 22 | "sdfdsf dsfds jfidso fjdsiofdsuifduifhdsuifhdsu ifdsuifhsui89ys9u uhuifhikhsuishafidskhdo sads " + 23 | "dskaop dksa0ksaopdisa0-disakdopsajdospdi0s-a dijks0pdjksds-aidk9saodjsadjs9dusaj0oidjhsdsdj9s adjsd90saidj"), nil 24 | } 25 | 26 | func (m *mock) Decrypt(conf *config.Configuration, ciphertext []byte) ([]byte, error) { 27 | return m.plaintext, nil 28 | } 29 | -------------------------------------------------------------------------------- /crypt/rsa/rsa.go: -------------------------------------------------------------------------------- 1 | package rsa 2 | 3 | import ( 4 | "context" 5 | "crypto/rand" 6 | "crypto/rsa" 7 | "crypto/sha256" 8 | "crypto/x509" 9 | "encoding/pem" 10 | "fmt" 11 | 12 | cloudkms "cloud.google.com/go/kms/apiv1" 13 | "cloud.google.com/go/kms/apiv1/kmspb" 14 | "google.golang.org/api/option" 15 | 16 | "obliviate/config" 17 | ) 18 | 19 | type EncryptionOnRest interface { 20 | Encrypt(*config.Configuration, []byte) ([]byte, error) 21 | Decrypt(*config.Configuration, []byte) ([]byte, error) 22 | } 23 | 24 | type Algorithm struct{} 25 | 26 | func NewAlgorithm() Algorithm { 27 | return Algorithm{} 28 | } 29 | 30 | // Encrypt will encrypt data locally using an 'RSA_DECRYPT_OAEP_2048_SHA256' 31 | // public key retrieved from Cloud KMS. 32 | func (Algorithm) Encrypt(conf *config.Configuration, plaintext []byte) ([]byte, error) { 33 | var err error 34 | var client *cloudkms.KeyManagementClient 35 | 36 | ctx := context.Background() 37 | if conf.ProdEnv { 38 | client, err = cloudkms.NewKeyManagementClient(ctx) 39 | } else { 40 | client, err = cloudkms.NewKeyManagementClient(ctx, option.WithCredentialsFile(conf.KmsCredentialFile)) 41 | } 42 | if err != nil { 43 | return nil, fmt.Errorf("cloudkms.NewKeyManagementClient: %v", err) 44 | } 45 | 46 | // Retrieve the public key from KMS. 47 | response, err := client.GetPublicKey(ctx, &kmspb.GetPublicKeyRequest{Name: conf.MasterKey}) 48 | if err != nil { 49 | return nil, fmt.Errorf("client.GetPublicKey: %v", err) 50 | } 51 | 52 | // Parse the key. 53 | block, _ := pem.Decode([]byte(response.Pem)) 54 | abstractKey, err := x509.ParsePKIXPublicKey(block.Bytes) 55 | if err != nil { 56 | return nil, fmt.Errorf("x509.ParsePKIXPublicKey: %+v", err) 57 | } 58 | 59 | rsaKey, ok := abstractKey.(*rsa.PublicKey) 60 | if !ok { 61 | return nil, fmt.Errorf("key %v is not EncryptionOnRest", conf.MasterKey) 62 | } 63 | 64 | // Encrypt data using the EncryptionOnRest public key. 65 | cipherText, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, rsaKey, plaintext, nil) 66 | if err != nil { 67 | return nil, fmt.Errorf("rsa.EncryptOAEP: %v", err) 68 | } 69 | 70 | return cipherText, nil 71 | } 72 | 73 | // Decrypt will attempt to decrypt a given ciphertext with an 74 | // private key stored on Cloud KMS. 75 | func (Algorithm) Decrypt(conf *config.Configuration, ciphertext []byte) ([]byte, error) { 76 | var err error 77 | var client *cloudkms.KeyManagementClient 78 | 79 | ctx := context.Background() 80 | if conf.ProdEnv { 81 | client, err = cloudkms.NewKeyManagementClient(ctx) 82 | } else { 83 | client, err = cloudkms.NewKeyManagementClient(ctx, option.WithCredentialsFile(conf.KmsCredentialFile)) 84 | } 85 | if err != nil { 86 | return nil, fmt.Errorf("cloudkms.NewKeyManagementClient: %v", err) 87 | } 88 | 89 | // Build the request. 90 | req := &kmspb.AsymmetricDecryptRequest{ 91 | Name: conf.MasterKey, 92 | Ciphertext: ciphertext, 93 | } 94 | // Call the API. 95 | response, err := client.AsymmetricDecrypt(ctx, req) 96 | if err != nil { 97 | return nil, fmt.Errorf("AsymmetricDecrypt: %v", err) 98 | } 99 | return response.Plaintext, nil 100 | } 101 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | 2 | #connect CLI with GCM: gcloud auth configure-docker 3 | docker build . --tag gcr.io/obliviate/obliviate 4 | docker push gcr.io/obliviate/obliviate 5 | 6 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module obliviate 2 | 3 | go 1.24 4 | 5 | require ( 6 | cloud.google.com/go/firestore v1.18.0 7 | cloud.google.com/go/kms v1.21.1 8 | firebase.google.com/go v3.13.0+incompatible 9 | github.com/go-chi/chi/v5 v5.2.1 10 | github.com/go-chi/cors v1.2.1 11 | github.com/stretchr/testify v1.10.0 12 | golang.org/x/crypto v0.37.0 13 | golang.org/x/crypto/x509roots/fallback v0.0.0-20250411160614-4bc071128182 14 | golang.org/x/text v0.24.0 15 | google.golang.org/api v0.228.0 16 | google.golang.org/grpc v1.71.1 17 | ) 18 | 19 | require ( 20 | cel.dev/expr v0.23.1 // indirect 21 | cloud.google.com/go v0.120.0 // indirect 22 | cloud.google.com/go/auth v0.15.0 // indirect 23 | cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect 24 | cloud.google.com/go/compute/metadata v0.6.0 // indirect 25 | cloud.google.com/go/iam v1.5.0 // indirect 26 | cloud.google.com/go/longrunning v0.6.6 // indirect 27 | cloud.google.com/go/monitoring v1.24.1 // indirect 28 | cloud.google.com/go/storage v1.51.0 // indirect 29 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 // indirect 30 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 // indirect 31 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 // indirect 32 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 33 | github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f // indirect 34 | github.com/davecgh/go-spew v1.1.1 // indirect 35 | github.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect 36 | github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect 37 | github.com/felixge/httpsnoop v1.0.4 // indirect 38 | github.com/go-logr/logr v1.4.2 // indirect 39 | github.com/go-logr/stdr v1.2.2 // indirect 40 | github.com/golang/protobuf v1.5.4 // indirect 41 | github.com/google/s2a-go v0.1.9 // indirect 42 | github.com/google/uuid v1.6.0 // indirect 43 | github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect 44 | github.com/googleapis/gax-go/v2 v2.14.1 // indirect 45 | github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect 46 | github.com/pmezard/go-difflib v1.0.0 // indirect 47 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 48 | go.opentelemetry.io/contrib/detectors/gcp v1.35.0 // indirect 49 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect 50 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect 51 | go.opentelemetry.io/otel v1.35.0 // indirect 52 | go.opentelemetry.io/otel/metric v1.35.0 // indirect 53 | go.opentelemetry.io/otel/sdk v1.35.0 // indirect 54 | go.opentelemetry.io/otel/sdk/metric v1.35.0 // indirect 55 | go.opentelemetry.io/otel/trace v1.35.0 // indirect 56 | golang.org/x/net v0.39.0 // indirect 57 | golang.org/x/oauth2 v0.29.0 // indirect 58 | golang.org/x/sync v0.13.0 // indirect 59 | golang.org/x/sys v0.32.0 // indirect 60 | golang.org/x/time v0.11.0 // indirect 61 | google.golang.org/appengine v1.6.8 // indirect 62 | google.golang.org/genproto v0.0.0-20250409194420-de1ac958c67a // indirect 63 | google.golang.org/genproto/googleapis/api v0.0.0-20250409194420-de1ac958c67a // indirect 64 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250409194420-de1ac958c67a // indirect 65 | google.golang.org/protobuf v1.36.6 // indirect 66 | gopkg.in/yaml.v3 v3.0.1 // indirect 67 | ) 68 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cel.dev/expr v0.23.1 h1:K4KOtPCJQjVggkARsjG9RWXP6O4R73aHeJMa/dmCQQg= 2 | cel.dev/expr v0.23.1/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= 3 | cloud.google.com/go v0.120.0 h1:wc6bgG9DHyKqF5/vQvX1CiZrtHnxJjBlKUyF9nP6meA= 4 | cloud.google.com/go v0.120.0/go.mod h1:/beW32s8/pGRuj4IILWQNd4uuebeT4dkOhKmkfit64Q= 5 | cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps= 6 | cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8= 7 | cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= 8 | cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= 9 | cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= 10 | cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= 11 | cloud.google.com/go/firestore v1.18.0 h1:cuydCaLS7Vl2SatAeivXyhbhDEIR8BDmtn4egDhIn2s= 12 | cloud.google.com/go/firestore v1.18.0/go.mod h1:5ye0v48PhseZBdcl0qbl3uttu7FIEwEYVaWm0UIEOEU= 13 | cloud.google.com/go/iam v1.5.0 h1:QlLcVMhbLGOjRcGe6VTGGTyQib8dRLK2B/kYNV0+2xs= 14 | cloud.google.com/go/iam v1.5.0/go.mod h1:U+DOtKQltF/LxPEtcDLoobcsZMilSRwR7mgNL7knOpo= 15 | cloud.google.com/go/kms v1.21.1 h1:r1Auo+jlfJSf8B7mUnVw5K0fI7jWyoUy65bV53VjKyk= 16 | cloud.google.com/go/kms v1.21.1/go.mod h1:s0wCyByc9LjTdCjG88toVs70U9W+cc6RKFc8zAqX7nE= 17 | cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc= 18 | cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA= 19 | cloud.google.com/go/longrunning v0.6.6 h1:XJNDo5MUfMM05xK3ewpbSdmt7R2Zw+aQEMbdQR65Rbw= 20 | cloud.google.com/go/longrunning v0.6.6/go.mod h1:hyeGJUrPHcx0u2Uu1UFSoYZLn4lkMrccJig0t4FI7yw= 21 | cloud.google.com/go/monitoring v1.24.1 h1:vKiypZVFD/5a3BbQMvI4gZdl8445ITzXFh257XBgrS0= 22 | cloud.google.com/go/monitoring v1.24.1/go.mod h1:Z05d1/vn9NaujqY2voG6pVQXoJGbp+r3laV+LySt9K0= 23 | cloud.google.com/go/storage v1.51.0 h1:ZVZ11zCiD7b3k+cH5lQs/qcNaoSz3U9I0jgwVzqDlCw= 24 | cloud.google.com/go/storage v1.51.0/go.mod h1:YEJfu/Ki3i5oHC/7jyTgsGZwdQ8P9hqMqvpi5kRKGgc= 25 | cloud.google.com/go/trace v1.11.5 h1:CALS1loyxJMnRiCwZSpdf8ac7iCsjreMxFD2WGxzzHU= 26 | cloud.google.com/go/trace v1.11.5/go.mod h1:TwblCcqNInriu5/qzaeYEIH7wzUcchSdeY2l5wL3Eec= 27 | firebase.google.com/go v3.13.0+incompatible h1:3TdYC3DDi6aHn20qoRkxwGqNgdjtblwVAyRLQwGn/+4= 28 | firebase.google.com/go v3.13.0+incompatible/go.mod h1:xlah6XbEyW6tbfSklcfe5FHJIwjt8toICdV5Wh9ptHs= 29 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0 h1:ErKg/3iS1AKcTkf3yixlZ54f9U1rljCkQyEXWUnIUxc= 30 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.27.0/go.mod h1:yAZHSGnqScoU556rBOVkwLze6WP5N+U11RHuWaGVxwY= 31 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0 h1:fYE9p3esPxA/C0rQ0AHhP0drtPXDRhaWiwg1DPqO7IU= 32 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.51.0/go.mod h1:BnBReJLvVYx2CS/UHOgVz2BXKXD9wsQPxZug20nZhd0= 33 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0 h1:OqVGm6Ei3x5+yZmSJG1Mh2NwHvpVmZ08CB5qJhT9Nuk= 34 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.51.0/go.mod h1:SZiPHWGOOk3bl8tkevxkoiwPgsIl6CwrWcbwjfHZpdM= 35 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0 h1:6/0iUd0xrnX7qt+mLNRwg5c0PGv8wpE8K90ryANQwMI= 36 | github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.51.0/go.mod h1:otE2jQekW/PqXk1Awf5lmfokJx4uwuqcj1ab5SpGeW0= 37 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 38 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 39 | github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f h1:C5bqEmzEPLsHm9Mv73lSE9e9bKV23aB1vxOsmZrkl3k= 40 | github.com/cncf/xds/go v0.0.0-20250326154945-ae57f3c0d45f/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= 41 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 42 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 43 | github.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M= 44 | github.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA= 45 | github.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A= 46 | github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw= 47 | github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= 48 | github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= 49 | github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= 50 | github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= 51 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 52 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 53 | github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= 54 | github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= 55 | github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= 56 | github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= 57 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 58 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 59 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 60 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 61 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 62 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 63 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 64 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 65 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 66 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 67 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 68 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 69 | github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= 70 | github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= 71 | github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= 72 | github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= 73 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 74 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 75 | github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= 76 | github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= 77 | github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= 78 | github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= 79 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 80 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 81 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 82 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 83 | github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= 84 | github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= 85 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 86 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 87 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 88 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 89 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 90 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 91 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 92 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 93 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 94 | go.opentelemetry.io/contrib/detectors/gcp v1.35.0 h1:bGvFt68+KTiAKFlacHW6AhA56GF2rS0bdD3aJYEnmzA= 95 | go.opentelemetry.io/contrib/detectors/gcp v1.35.0/go.mod h1:qGWP8/+ILwMRIUf9uIVLloR1uo5ZYAslM4O6OqUi1DA= 96 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= 97 | go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= 98 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= 99 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= 100 | go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= 101 | go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= 102 | go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0 h1:WDdP9acbMYjbKIyJUhTvtzj601sVJOqgWdUxSdR/Ysc= 103 | go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.29.0/go.mod h1:BLbf7zbNIONBLPwvFnwNHGj4zge8uTCM/UPIVW1Mq2I= 104 | go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= 105 | go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= 106 | go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY= 107 | go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg= 108 | go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o= 109 | go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w= 110 | go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= 111 | go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= 112 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 113 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 114 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 115 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 116 | golang.org/x/crypto/x509roots/fallback v0.0.0-20250411160614-4bc071128182 h1:9Pg8m5wg1t5BH0Jwoal8NtHe4EmFudsF59BR3hk19UQ= 117 | golang.org/x/crypto/x509roots/fallback v0.0.0-20250411160614-4bc071128182/go.mod h1:lxN5T34bK4Z/i6cMaU7frUU57VkDXFD4Kamfl/cp9oU= 118 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 119 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 120 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 121 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 122 | golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= 123 | golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= 124 | golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= 125 | golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= 126 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 127 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 128 | golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= 129 | golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 130 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 131 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 132 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 133 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 134 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 135 | golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= 136 | golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 137 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 138 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 139 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 140 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 141 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 142 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 143 | golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 144 | golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 145 | golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= 146 | golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 147 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 148 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 149 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 150 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 151 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 152 | google.golang.org/api v0.228.0 h1:X2DJ/uoWGnY5obVjewbp8icSL5U4FzuCfy9OjbLSnLs= 153 | google.golang.org/api v0.228.0/go.mod h1:wNvRS1Pbe8r4+IfBIniV8fwCpGwTrYa+kMUDiC5z5a4= 154 | google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= 155 | google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= 156 | google.golang.org/genproto v0.0.0-20250409194420-de1ac958c67a h1:AoyioNVZR+nS6zbvnvW5rjQdeQu7/BWwIT7YI8Gq5wU= 157 | google.golang.org/genproto v0.0.0-20250409194420-de1ac958c67a/go.mod h1:qD4k1RhYfNmRjqaHJxKLG/HRtqbXVclhjop2mPlxGwA= 158 | google.golang.org/genproto/googleapis/api v0.0.0-20250409194420-de1ac958c67a h1:OQ7sHVzkx6L57dQpzUS4ckfWJ51KDH74XHTDe23xWAs= 159 | google.golang.org/genproto/googleapis/api v0.0.0-20250409194420-de1ac958c67a/go.mod h1:2R6XrVC8Oc08GlNh8ujEpc7HkLiEZ16QeY7FxIs20ac= 160 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250409194420-de1ac958c67a h1:GIqLhp/cYUkuGuiT+vJk8vhOP86L4+SP5j8yXgeVpvI= 161 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250409194420-de1ac958c67a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= 162 | google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI= 163 | google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= 164 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 165 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 166 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 167 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 168 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 169 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 170 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 171 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 172 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 173 | -------------------------------------------------------------------------------- /handler/handler_test.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/rand" 7 | "crypto/sha1" 8 | "encoding/json" 9 | "fmt" 10 | "net/http" 11 | "net/http/httptest" 12 | "os" 13 | "testing" 14 | "time" 15 | 16 | "github.com/stretchr/testify/assert" 17 | "golang.org/x/crypto/nacl/box" 18 | "golang.org/x/crypto/nacl/secretbox" 19 | 20 | "obliviate/app" 21 | "obliviate/config" 22 | "obliviate/crypt" 23 | "obliviate/crypt/rsa" 24 | "obliviate/handler/webModels" 25 | "obliviate/repository" 26 | "obliviate/repository/mock" 27 | ) 28 | 29 | type testParams struct { 30 | status int 31 | message string 32 | } 33 | 34 | var conf *config.Configuration 35 | var db repository.DataBase 36 | 37 | var params = []testParams{ 38 | 39 | {http.StatusOK, "wiadomość"}, 40 | {http.StatusOK, "Facebook i Instagram deklarują w swoich regułach, że nie chcą być agencją rekrutacyjną biznesu pornograficznego ani robić za sutenera. " + 41 | "Zgodnie z wytycznymi więc oferowanie lub szukanie nagich zdjęć, rozmów erotycznych lub po prostu partnera czy partnerki seksualnej przez wymienione platformy jest zakazane. " + 42 | "Używanie do tego ikon emoji specyficznych dla danego kontekstu i powszechnie uważanych za nacechowane seksualnie jest, jak deklaruje platforma, dużym przewinieniem. " + 43 | "Na tyle dużym, że może się skończyć nie tylko ostrzeżeniem, ale wręcz blokadą konta. Chodzi tu między innymi o niewinną tylko z pozoru brzoskwinkę, lśniącego bakłażana " + 44 | "czy życiodajną kroplę wody."}, 45 | {http.StatusOK, "الخطوط الجوية الفرنسية أو إير فرانس من بين أكبر شركات الطيران في العالم. والمقر " + 46 | "الرئيسي للشركة في باريس، وهي تابعة لشركة الخطوط الجوية الفرنسية - كيه إل إم، وتنظم الخطوط"}, 47 | {http.StatusOK, "에어 프랑스(프랑스어: Air France 에르 프랑스[*])는 에어 프랑스-KLM의 사업부로 KLM을 합병하기 전에는 프랑스의 국책 항공사였으며, 2009년 9월 기준 종업원수는 60,686명이다[1]. " + 48 | "본사는 파리 시 근교의 샤를 드 골 공항에 있으며 현재는 에어 프랑스-KLM이 쓰고 있다. 2001년 4월부터 2002년 3월까지 4330만명의 승객을 실어 나르고 125억3천만 유로를 벌어들였다. " + 49 | "에어 프랑스의 자회사 레지오날은 주로 유럽 내에서 제트 비행기와 터보프롭 비행기로 지역 항공 노선을 운항하고 있다."}, 50 | } 51 | 52 | func init() { 53 | conf = &config.Configuration{ 54 | DefaultDurationTime: time.Hour * 24 * 7, 55 | ProdEnv: os.Getenv("ENV") == "PROD", 56 | MasterKey: os.Getenv("HSM_MASTER_KEY"), 57 | KmsCredentialFile: os.Getenv("KMS_CREDENTIAL_FILE"), 58 | FirestoreCredentialFile: os.Getenv("FIRESTORE_CREDENTIAL_FILE"), 59 | } 60 | // db = repository.NewConnection(context.Background(), "local", conf.FirestoreCredentialFile, os.Getenv("OBLIVIATE_PROJECT_ID"), conf.ProdEnv) 61 | db = mock.StorageMock() 62 | 63 | } 64 | 65 | func TestEncodeDecodeMessage(t *testing.T) { 66 | rsa := rsa.NewMockAlgorithm() 67 | // rsa := rsa.NewAlgorithm() 68 | keys, err := crypt.NewKeys(db, conf, rsa, true) 69 | if err != nil { 70 | panic("cannot create key pair") 71 | } 72 | app := app.NewApp(db, conf, keys) 73 | 74 | for _, tab := range params { 75 | 76 | browserPublicKey, _, _ := box.GenerateKey(rand.Reader) 77 | _, messageSecretKey, _ := box.GenerateKey(rand.Reader) 78 | messageNonce, _ := keys.GenerateNonce() 79 | 80 | messageWithNonce := secretbox.Seal(messageNonce[:], []byte(tab.message), &messageNonce, messageSecretKey) 81 | messageWithSecret := append(messageSecretKey[:], messageWithNonce[24:]...) // take it without nonce 82 | 83 | transmissionNonce, _ := keys.GenerateNonce() 84 | encryptedTransmission := box.Seal(transmissionNonce[:], messageWithSecret, &transmissionNonce, browserPublicKey, keys.PrivateKey) 85 | 86 | saveRequest := webModels.SaveRequest{ 87 | Message: encryptedTransmission[24:], // take it without nonce, will be base64ed on marshal 88 | TransmissionNonce: transmissionNonce[:], 89 | Hash: makeHash(messageNonce), 90 | PublicKey: browserPublicKey[:], 91 | } 92 | 93 | code, _ := makePost(t, jsonFromStruct(context.Background(), saveRequest), Save(app)) 94 | assert.Equal(t, tab.status, code, "response code not expected") 95 | 96 | // read 97 | 98 | browserPublicKey, browserPrivateKey, _ := box.GenerateKey(rand.Reader) // new keys 99 | 100 | readRequest := webModels.ReadRequest{ 101 | Hash: makeHash(messageNonce), 102 | PublicKey: browserPublicKey[:], 103 | } 104 | 105 | code, readResponse := makePost(t, jsonFromStruct(context.Background(), readRequest), Read(app)) 106 | assert.Equal(t, tab.status, code, "response code not expected") 107 | 108 | if code != http.StatusOK { 109 | continue 110 | } 111 | 112 | data := webModels.ReadResponse{} 113 | err := json.Unmarshal([]byte(readResponse), &data) 114 | assert.NoError(t, err, "error unmarshal read response") 115 | if err != nil { 116 | continue 117 | } 118 | 119 | encryptedTransmissionWithNonce := data.Message 120 | 121 | copy(transmissionNonce[:], encryptedTransmissionWithNonce) 122 | decryptedTransmission, ok := box.Open(nil, encryptedTransmissionWithNonce[24:], &transmissionNonce, keys.PublicKey, browserPrivateKey) 123 | assert.True(t, ok) 124 | 125 | copy(messageSecretKey[:], decryptedTransmission) 126 | 127 | decryptedMessage, ok := secretbox.Open(nil, decryptedTransmission[32:], &messageNonce, messageSecretKey) 128 | assert.True(t, ok, "error opening secretbox") 129 | if !ok { 130 | continue 131 | } 132 | 133 | assert.Equal(t, string(decryptedMessage), tab.message, "decrypted message is not the same") 134 | } 135 | } 136 | 137 | func makePost(t *testing.T, jsonMessage []byte, handler http.Handler) (int, string) { 138 | req, err := http.NewRequest("POST", "/post", bytes.NewBuffer(jsonMessage)) 139 | if err != nil { 140 | t.Fatal(err) 141 | } 142 | r := httptest.NewRecorder() 143 | handler.ServeHTTP(r, req) 144 | return r.Code, r.Body.String() 145 | } 146 | 147 | func makeHash(in [24]byte) string { 148 | h := sha1.New() 149 | h.Write(in[:]) 150 | bs := h.Sum(nil) 151 | return fmt.Sprintf("%x", bs) 152 | } 153 | -------------------------------------------------------------------------------- /handler/helpers.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "log/slog" 7 | "net/http" 8 | 9 | "obliviate/logs" 10 | ) 11 | 12 | func setStatusAndHeader(w http.ResponseWriter, status int, prodEnv bool) { 13 | w.Header().Set("Content-Type", "application/json") 14 | w.WriteHeader(status) 15 | } 16 | 17 | func jsonFromStruct(ctx context.Context, s interface{}) []byte { 18 | j, err := json.Marshal(s) 19 | if err != nil { 20 | slog.ErrorContext(ctx, "cannot marshal json", logs.Error, err, logs.JSON, s) 21 | } 22 | return j 23 | } 24 | 25 | func finishRequestWithErr(ctx context.Context, w http.ResponseWriter, msg string, status int, prodEnv bool) { 26 | slog.ErrorContext(ctx, msg) 27 | setStatusAndHeader(w, status, prodEnv) 28 | //nolint:errcheck 29 | w.Write([]byte("")) 30 | } 31 | 32 | func finishRequestWithWarn(ctx context.Context, w http.ResponseWriter, msg string, status int, prodEnv bool) { 33 | slog.WarnContext(ctx, msg) 34 | setStatusAndHeader(w, status, prodEnv) 35 | //nolint:errcheck 36 | w.Write([]byte("")) 37 | } 38 | -------------------------------------------------------------------------------- /handler/message.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "log/slog" 8 | "net/http" 9 | "obliviate/logs" 10 | "text/template" 11 | 12 | "obliviate/app" 13 | "obliviate/config" 14 | "obliviate/handler/webModels" 15 | "obliviate/i18n" 16 | ) 17 | 18 | const ( 19 | jsonErrMsg = "input json error" 20 | emptyBody = "empty body post, no json expected" 21 | ) 22 | 23 | func ProcessTemplate(config *config.Configuration, publicKey string) http.HandlerFunc { 24 | 25 | var t *template.Template 26 | if config.ProdEnv { 27 | t = template.Must(template.New("variables.json").ParseFS(config.EmbededStaticFiles, "variables.json")) 28 | } 29 | 30 | translation := i18n.NewTranslation() 31 | 32 | return func(w http.ResponseWriter, r *http.Request) { 33 | ctx := r.Context() 34 | slog.InfoContext(ctx, "ProcessTemplate Handler") 35 | 36 | if !config.ProdEnv { 37 | t, _ = template.New("variables.json").ParseFS(config.EmbededStaticFiles, "variables.json") 38 | } 39 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 40 | w.Header().Set("Cache-Control", "no-cache, no-store") 41 | w.Header().Set("Expires", "0") 42 | 43 | data := translation.GetTranslation(ctx, r.Header.Get("Accept-Language")) 44 | data["PublicKey"] = publicKey 45 | err := t.Execute(w, data) 46 | if err != nil { 47 | slog.ErrorContext(ctx, "Count not execute the template", logs.Error, err, logs.TemplateData, data) 48 | panic("Count not execute the template") 49 | } 50 | } 51 | } 52 | 53 | func Save(app *app.App) http.HandlerFunc { 54 | 55 | return func(w http.ResponseWriter, r *http.Request) { 56 | ctx := r.Context() 57 | slog.InfoContext(ctx, "Save handler...") 58 | 59 | defer r.Body.Close() 60 | if r.Body == nil { 61 | finishRequestWithErr(ctx, w, emptyBody, http.StatusBadRequest, app.Config.ProdEnv) 62 | return 63 | } 64 | 65 | data := webModels.SaveRequest{} 66 | err := json.NewDecoder(r.Body).Decode(&data) 67 | switch { 68 | case err != nil: 69 | finishRequestWithErr(ctx, w, jsonErrMsg, http.StatusBadRequest, app.Config.ProdEnv) 70 | case len(data.Message) == 0 || len(data.Message) > 256*1024*4: 71 | finishRequestWithErr(ctx, w, fmt.Sprintf("Message len is wrong = %d", len(data.Message)), http.StatusBadRequest, app.Config.ProdEnv) 72 | case len(data.TransmissionNonce) == 0: 73 | finishRequestWithErr(ctx, w, "TransmissionNonce is empty", http.StatusBadRequest, app.Config.ProdEnv) 74 | case len(data.Hash) == 0: 75 | finishRequestWithErr(ctx, w, "Hash is empty", http.StatusBadRequest, app.Config.ProdEnv) 76 | case len(data.TransmissionNonce) != 24: 77 | finishRequestWithErr(ctx, w, "TransmissionNonce length is wrong !=24", http.StatusBadRequest, app.Config.ProdEnv) 78 | case len(data.PublicKey) != 32: 79 | finishRequestWithErr(ctx, w, "PublicKey length is wrong !=24", http.StatusBadRequest, app.Config.ProdEnv) 80 | default: 81 | ctx := context.WithValue(ctx, config.AcceptLanguage, r.Header.Get("Accept-Language")) 82 | ctx = context.WithValue(ctx, config.CountryCode, r.Header.Get("CF-IPCountry")) 83 | err = app.ProcessSave(ctx, data) 84 | if err != nil { 85 | finishRequestWithErr(ctx, w, fmt.Sprintf("Cannot process input message, err: %v", err), http.StatusBadRequest, app.Config.ProdEnv) 86 | return 87 | } 88 | setStatusAndHeader(w, http.StatusOK, app.Config.ProdEnv) 89 | //nolint:errcheck 90 | w.Write([]byte("[]")) 91 | } 92 | } 93 | } 94 | 95 | func Read(app *app.App) http.HandlerFunc { 96 | 97 | return func(w http.ResponseWriter, r *http.Request) { 98 | ctx := r.Context() 99 | slog.InfoContext(ctx, "Read handler") 100 | 101 | defer r.Body.Close() 102 | if r.Body == nil { 103 | finishRequestWithErr(ctx, w, emptyBody, http.StatusBadRequest, app.Config.ProdEnv) 104 | return 105 | } 106 | 107 | data := webModels.ReadRequest{} 108 | err := json.NewDecoder(r.Body).Decode(&data) 109 | switch { 110 | case err != nil: 111 | finishRequestWithErr(ctx, w, jsonErrMsg, http.StatusBadRequest, app.Config.ProdEnv) 112 | case len(data.Hash) == 0: 113 | finishRequestWithErr(ctx, w, "Hash not found", http.StatusBadRequest, app.Config.ProdEnv) 114 | case len(data.PublicKey) == 0: 115 | finishRequestWithErr(ctx, w, "PublicKey not found", http.StatusBadRequest, app.Config.ProdEnv) 116 | case len(data.PublicKey) != 32: 117 | finishRequestWithErr(ctx, w, "PublicKey length is wrong !=32", http.StatusBadRequest, app.Config.ProdEnv) 118 | default: 119 | encrypted, costFactor, err := app.ProcessRead(ctx, data) 120 | if err != nil { 121 | finishRequestWithErr(ctx, w, fmt.Sprintf("Cannot process read message, err: %v", err), http.StatusBadRequest, app.Config.ProdEnv) 122 | return 123 | } 124 | if encrypted == nil { 125 | // not found 126 | finishRequestWithWarn(ctx, w, "Message not found", http.StatusNotFound, app.Config.ProdEnv) 127 | return 128 | } 129 | 130 | message := webModels.ReadResponse{Message: encrypted, CostFactor: costFactor} 131 | 132 | setStatusAndHeader(w, http.StatusOK, app.Config.ProdEnv) 133 | //nolint:errcheck 134 | w.Write(jsonFromStruct(ctx, message)) 135 | } 136 | } 137 | } 138 | 139 | func Delete(app *app.App) http.HandlerFunc { 140 | 141 | return func(w http.ResponseWriter, r *http.Request) { 142 | ctx := r.Context() 143 | slog.InfoContext(ctx, "Delete Handler") 144 | 145 | defer r.Body.Close() 146 | if r.Body == nil { 147 | finishRequestWithErr(ctx, w, emptyBody, http.StatusBadRequest, app.Config.ProdEnv) 148 | return 149 | } 150 | 151 | data := webModels.DeleteRequest{} 152 | err := json.NewDecoder(r.Body).Decode(&data) 153 | if err != nil { 154 | finishRequestWithErr(ctx, w, jsonErrMsg, http.StatusBadRequest, app.Config.ProdEnv) 155 | return 156 | } 157 | if len(data.Hash) == 0 { 158 | finishRequestWithErr(ctx, w, "Hash is empty", http.StatusBadRequest, app.Config.ProdEnv) 159 | return 160 | } 161 | 162 | app.ProcessDelete(r.Context(), data.Hash) 163 | 164 | setStatusAndHeader(w, http.StatusOK, app.Config.ProdEnv) 165 | //nolint:errcheck 166 | w.Write([]byte("[]")) 167 | } 168 | } 169 | 170 | func Expired(app *app.App) http.HandlerFunc { 171 | 172 | return func(w http.ResponseWriter, r *http.Request) { 173 | ctx := r.Context() 174 | slog.InfoContext(ctx, "Expired handler") 175 | 176 | if err := app.ProcessDeleteExpired(ctx); err != nil { 177 | slog.ErrorContext(ctx, "delete expired error", logs.Error, err.Error()) 178 | setStatusAndHeader(w, http.StatusInternalServerError, app.Config.ProdEnv) 179 | } else { 180 | setStatusAndHeader(w, http.StatusOK, app.Config.ProdEnv) 181 | } 182 | //nolint:errcheck 183 | w.Write([]byte("[]")) 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /handler/static.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "io/fs" 5 | "log/slog" 6 | "net/http" 7 | "obliviate/logs" 8 | "os" 9 | 10 | "obliviate/config" 11 | ) 12 | 13 | func StaticFiles(config *config.Configuration, useEmbedFS bool) http.HandlerFunc { 14 | fs := http.FileServer(getStaticsFS(config.EmbededStaticFiles, useEmbedFS, config.StaticFilesLocation)) 15 | 16 | return func(w http.ResponseWriter, r *http.Request) { 17 | fs.ServeHTTP(w, r) 18 | } 19 | } 20 | 21 | func getStaticsFS(static fs.FS, useEmbedFS bool, stripPath string) http.FileSystem { 22 | if !useEmbedFS { 23 | slog.Info("using live os files mode") 24 | return http.FS(os.DirFS(stripPath)) 25 | } 26 | 27 | slog.Info("using embed files mode") 28 | fsys, err := fs.Sub(static, stripPath) 29 | if err != nil { 30 | slog.Error("FS error", logs.Error, err) 31 | } 32 | 33 | return http.FS(fsys) 34 | } 35 | -------------------------------------------------------------------------------- /handler/webModels/main.go: -------------------------------------------------------------------------------- 1 | package webModels 2 | 3 | type SaveRequest struct { 4 | Message []byte `json:"message"` 5 | TransmissionNonce []byte `json:"nonce"` 6 | Hash string `json:"hash"` 7 | PublicKey []byte `json:"publicKey"` 8 | Time int `json:"time"` 9 | CostFactor int `json:"costFactor"` 10 | } 11 | 12 | type ReadRequest struct { 13 | Hash string `json:"hash"` 14 | PublicKey []byte `json:"publicKey"` 15 | Password bool `json:"password"` 16 | } 17 | 18 | type DeleteRequest struct { 19 | Hash string `json:"hash"` 20 | } 21 | 22 | type ReadResponse struct { 23 | Message []byte `json:"message"` 24 | CostFactor int `json:"costFactor,omitempty"` 25 | } 26 | -------------------------------------------------------------------------------- /i18n/entries.go: -------------------------------------------------------------------------------- 1 | package i18n 2 | 3 | import "golang.org/x/text/language" 4 | 5 | type entry struct { 6 | key string 7 | msg string 8 | } 9 | 10 | var translationsSet = map[language.Tag][]entry{ 11 | language.English: { 12 | {"title", "Private and secure notes - send your secrets safely"}, 13 | {"header", "Private secure notes"}, 14 | {"description", "Secure encrypted messages"}, 15 | {"enterTextMessage", "Enter the message to encrypt"}, 16 | {"secureButton", "Encrypt the message"}, 17 | {"copyLink", "Copy the link and send it to a friend. The message will be deleted immediately after being read or after 4 weeks."}, 18 | {"copyLinkButton", "Copy link"}, 19 | {"newMessageButton", "New message"}, 20 | {"decodedMessage", "Decrypted message"}, 21 | {"messageRead", "Message read, expired, or the link is incorrect"}, 22 | {"readMessageButton", "Decrypt message"}, 23 | {"infoHeader", "description"}, 24 | {"info", "The tool uses various encryption methods to ensure maximum security. " + 25 | "Without the link, it is not possible to decrypt the message. Use a password to further increase security. " + 26 | "The source code of the tool is open and can be viewed on"}, 27 | {"info1", ". If you want to contact us, send us a"}, 28 | {"info2", "message"}, 29 | {"info3", "."}, 30 | {"generalError", "Something went wrong. Please try again later."}, 31 | {"encryptNetworkError", "Something went wrong. I cannot save the message. Please try again."}, 32 | {"decryptNetworkError", "Something went wrong. I cannot read the message. Please try again."}, 33 | {"password", "Password"}, 34 | {"enterPasswordPlaceholder", "enter the password"}, 35 | {"linkIsCorrupted", "The link is damaged"}, 36 | }, 37 | language.German: { 38 | {"title", "Private und sichere Notizen - Versenden Sie Ihre Geheimnisse sicher"}, 39 | {"header", "Private sichere Notizen"}, 40 | {"description", "Sichere verschlüsselte Nachrichten"}, 41 | {"enterTextMessage", "Geben Sie die zu verschlüsselnde Nachricht ein"}, 42 | {"secureButton", "Nachricht verschlüsseln"}, 43 | {"copyLink", "Kopieren Sie den Link und senden Sie ihn an einen Freund. Die Nachricht wird sofort nach dem Lesen oder nach 4 Wochen gelöscht."}, 44 | {"copyLinkButton", "Link kopieren"}, 45 | {"newMessageButton", "Neue Nachricht"}, 46 | {"decodedMessage", "Entschlüsselte Nachricht"}, 47 | {"messageRead", "Nachricht gelesen, abgelaufen oder der Link ist falsch"}, 48 | {"readMessageButton", "Nachricht entschlüsseln"}, 49 | {"infoHeader", "Beschreibung"}, 50 | {"info", "Das Tool verwendet verschiedene Verschlüsselungsmethoden, um maximale Sicherheit zu gewährleisten. " + 51 | "Ohne den Link ist es nicht möglich, die Nachricht zu entschlüsseln. Verwenden Sie ein Passwort, um die Sicherheit weiter zu erhöhen. " + 52 | "Der Quellcode des Tools ist offen und kann auf"}, 53 | {"info1", " eingesehen werden. Wenn Sie uns kontaktieren möchten, senden Sie uns eine"}, 54 | {"info2", "Nachricht"}, 55 | {"info3", "."}, 56 | {"generalError", "Etwas ist schief gelaufen. Bitte versuchen Sie es später noch einmal."}, 57 | {"encryptNetworkError", "Etwas ist schief gelaufen. Ich kann die Nachricht nicht speichern. Bitte versuchen Sie es noch einmal."}, 58 | {"decryptNetworkError", "Etwas ist schief gelaufen. Ich kann die Nachricht nicht lesen. Bitte versuchen Sie es noch einmal."}, 59 | {"password", "Passwort"}, 60 | {"enterPasswordPlaceholder", "Geben Sie das Passwort ein"}, 61 | {"linkIsCorrupted", "Der Link ist beschädigt"}, 62 | }, 63 | language.Dutch: { 64 | {"title", "Privé en veilige notities - stuur je geheimen veilig"}, 65 | {"header", "Privé veilige notities"}, 66 | {"description", "Beveiligde versleutelde berichten"}, 67 | {"enterTextMessage", "Voer het bericht in om te versleutelen"}, 68 | {"secureButton", "Versleutel het bericht"}, 69 | {"copyLink", "Kopieer de link en stuur deze naar een vriend. Het bericht wordt onmiddellijk verwijderd nadat het is gelezen of na 4 weken."}, 70 | {"copyLinkButton", "Kopieer link"}, 71 | {"newMessageButton", "Nieuw bericht"}, 72 | {"decodedMessage", "Ontsleuteld bericht"}, 73 | {"messageRead", "Bericht gelezen, verlopen of de link is onjuist"}, 74 | {"readMessageButton", "Ontsleutel bericht"}, 75 | {"infoHeader", "beschrijving"}, 76 | {"info", "De tool gebruikt verschillende versleutelingsmethoden om maximale beveiliging te garanderen. " + 77 | "Zonder de link is het niet mogelijk om het bericht te ontsleutelen. Gebruik een wachtwoord om de beveiliging verder te verhogen. " + 78 | "De broncode van de tool is open en kan worden bekeken op"}, 79 | {"info1", ". Als u contact met ons wilt opnemen, stuur ons dan een"}, 80 | {"info2", "bericht"}, 81 | {"info3", "."}, 82 | {"generalError", "Er is iets misgegaan. Probeer het later opnieuw."}, 83 | {"encryptNetworkError", "Er is iets misgegaan. Ik kan het bericht niet opslaan. Probeer het opnieuw."}, 84 | {"decryptNetworkError", "Er is iets misgegaan. Ik kan het bericht niet lezen. Probeer het opnieuw."}, 85 | {"password", "Wachtwoord"}, 86 | {"enterPasswordPlaceholder", "voer het wachtwoord in"}, 87 | {"linkIsCorrupted", "De link is beschadigd"}, 88 | }, 89 | language.Danish: { 90 | {"title", "Private og sikre noter - send dine hemmeligheder sikkert"}, 91 | {"header", "Private sikre noter"}, 92 | {"description", "Sikre krypterede beskeder"}, 93 | {"enterTextMessage", "Indtast beskeden, der skal krypteres"}, 94 | {"secureButton", "Kryptér beskeden"}, 95 | {"copyLink", "Kopier linket og send det til en ven. Beskeden vil blive slettet umiddelbart efter at være blevet læst eller efter 4 uger."}, 96 | {"copyLinkButton", "Kopiér link"}, 97 | {"newMessageButton", "Ny besked"}, 98 | {"decodedMessage", "Dekrypteret besked"}, 99 | {"messageRead", "Besked læst, udløbet, eller linket er forkert"}, 100 | {"readMessageButton", "Dekryptér besked"}, 101 | {"infoHeader", "beskrivelse"}, 102 | {"info", "Værktøjet bruger forskellige krypteringsmetoder for at sikre maksimal sikkerhed. " + 103 | "Uden linket er det ikke muligt at dekryptere beskeden. Brug en adgangskode for at øge sikkerheden yderligere. " + 104 | "Kildekoden til værktøjet er åben og kan ses på"}, 105 | {"info1", ". Hvis du ønsker at kontakte os, send os en"}, 106 | {"info2", "besked"}, 107 | {"info3", "."}, 108 | {"generalError", "Noget gik galt. Prøv venligst igen senere."}, 109 | {"encryptNetworkError", "Noget gik galt. Jeg kan ikke gemme beskeden. Prøv igen."}, 110 | {"decryptNetworkError", "Noget gik galt. Jeg kan ikke læse beskeden. Prøv igen."}, 111 | {"password", "Adgangskode"}, 112 | {"enterPasswordPlaceholder", "indtast adgangskoden"}, 113 | {"linkIsCorrupted", "Linket er beskadiget"}, 114 | }, 115 | language.Norwegian: { 116 | {"title", "Private og sikre notater - send hemmelighetene dine trygt"}, 117 | {"header", "Private, sikre notater"}, 118 | {"description", "Sikkert meldingkrypteringsverktøy som selvdestruerer"}, 119 | {"enterTextMessage", "Tast inn melding for kryptering"}, 120 | {"secureButton", "Krypter melding"}, 121 | {"copyLink", "Kopier lenke og send til en venn. Meldingen vil bli slettet etter å ha blitt lest eller etter 4 uker uten å ha blitt åpnet"}, 122 | {"copyLinkButton", "Kopier lenke"}, 123 | {"newMessageButton", "Ny melding"}, 124 | {"decodedMessage", "Dekrypter melding"}, 125 | {"messageRead", "Melding har allerede blitt lest, slettet eller lenke er korrumpert"}, 126 | {"readMessageButton", "Les melding"}, 127 | {"infoHeader", "Info om"}, 128 | {"info", "Dette verktøyet er laget med hensyn og respekt med tanke på ditt personvern. " + 129 | "For bedre sikkerhet, vennligst bruk et passord. Verktøyet er open-source og lesbar for offentligheten. " + 130 | "Koden er tilgjengelig på"}, 131 | {"info1", ". Om du ønsker å ta kontakt, vennligst send oss "}, 132 | {"info2", "e-post"}, 133 | {"info3", "."}, 134 | {"generalError", "Noe gikk galt. Vennligst prøv igjen senere"}, 135 | {"encryptNetworkError", "Noe gikk galt, meldingen kunne ikke bli lagret. Vennligst prøv igjen."}, 136 | {"decryptNetworkError", "Noe gikk galt, meldingen kunne ikke bli lastet inn. Vennligst prøv igjen."}, 137 | {"password", "Passord"}, 138 | {"enterPasswordPlaceholder", "Tast inn passord"}, 139 | {"linkIsCorrupted", "Lenken er korrumpert"}, 140 | }, 141 | language.Swedish: { 142 | {"title", "Privata och säkra anteckningar - skicka dina hemligheter säkert"}, 143 | {"header", "Privata säkra anteckningar"}, 144 | {"description", "Säkra krypterade meddelanden"}, 145 | {"enterTextMessage", "Ange meddelandet att kryptera"}, 146 | {"secureButton", "Kryptera meddelandet"}, 147 | {"copyLink", "Kopiera länken och skicka den till en vän. Meddelandet raderas omedelbart efter att det har lästs eller efter 4 veckor."}, 148 | {"copyLinkButton", "Kopiera länk"}, 149 | {"newMessageButton", "Nytt meddelande"}, 150 | {"decodedMessage", "Dekrypterat meddelande"}, 151 | {"messageRead", "Meddelandet har lästs, utgått eller länken är felaktig"}, 152 | {"readMessageButton", "Dekryptera meddelandet"}, 153 | {"infoHeader", "beskrivning"}, 154 | {"info", "Verktyget använder olika krypteringsmetoder för att säkerställa maximal säkerhet. " + 155 | "Utan länken är det inte möjligt att dekryptera meddelandet. Använd ett lösenord för att ytterligare öka säkerheten. " + 156 | "Källkoden för verktyget är öppen och kan ses på"}, 157 | {"info1", ". Om du vill kontakta oss, skicka oss ett"}, 158 | {"info2", "meddelande"}, 159 | {"info3", "."}, 160 | {"generalError", "Något gick fel. Försök igen senare."}, 161 | {"encryptNetworkError", "Något gick fel. Jag kan inte spara meddelandet. Försök igen."}, 162 | {"decryptNetworkError", "Något gick fel. Jag kan inte läsa meddelandet. Försök igen."}, 163 | {"password", "Lösenord"}, 164 | {"enterPasswordPlaceholder", "ange lösenordet"}, 165 | {"linkIsCorrupted", "Länken är skadad"}, 166 | }, 167 | language.Finnish: { 168 | {"title", "Yksityiset ja turvalliset muistiinpanot - lähetä salaisuutesi turvallisesti"}, 169 | {"header", "Yksityiset turvalliset muistiinpanot"}, 170 | {"description", "Turvalliset salatut viestit"}, 171 | {"enterTextMessage", "Anna salattava viesti"}, 172 | {"secureButton", "Salaa viesti"}, 173 | {"copyLink", "Kopioi linkki ja lähetä se ystävällesi. Viesti poistetaan heti sen luettuaan tai 4 viikon kuluttua."}, 174 | {"copyLinkButton", "Kopioi linkki"}, 175 | {"newMessageButton", "Uusi viesti"}, 176 | {"decodedMessage", "Salauksen purkamisen viesti"}, 177 | {"messageRead", "Viesti luettu, vanhentunut tai linkki on virheellinen"}, 178 | {"readMessageButton", "Pura viestin salaus"}, 179 | {"infoHeader", "kuvaus"}, 180 | {"info", "Työkalu käyttää erilaisia salausmenetelmiä maksimaalisen turvallisuuden varmistamiseksi. " + 181 | "Ilman linkkiä viestiä ei voi purkaa. Käytä salasanaa lisätäksesi turvallisuutta. " + 182 | "Työkalun lähdekoodi on avoin ja sen voi tarkastella"}, 183 | {"info1", ". Jos haluat ottaa meihin yhteyttä, lähetä meille"}, 184 | {"info2", "viesti"}, 185 | {"info3", "."}, 186 | {"generalError", "Jotain meni pieleen. Yritä uudelleen myöhemmin."}, 187 | {"encryptNetworkError", "Jotain meni pieleen. En voi tallentaa viestiä. Yritä uudelleen."}, 188 | {"decryptNetworkError", "Jotain meni pieleen. En voi lukea viestiä. Yritä uudelleen."}, 189 | {"password", "Salasana"}, 190 | {"enterPasswordPlaceholder", "anna salasana"}, 191 | {"linkIsCorrupted", "Linkki on vioittunut"}, 192 | }, 193 | language.Estonian: { 194 | {"title", "Privaatsed ja turvalised märkmed - saatke oma saladused turvaliselt"}, 195 | {"header", "Privaatsed turvalised märkmed"}, 196 | {"description", "Turvalised krüpteeritud sõnumid"}, 197 | {"enterTextMessage", "Sisestage krüpteeritav sõnum"}, 198 | {"secureButton", "Krüpteeri sõnum"}, 199 | {"copyLink", "Kopeeri link ja saatke see sõbrale. Sõnum kustutatakse kohe pärast lugemist või 4 nädala möödumist."}, 200 | {"copyLinkButton", "Kopeeri link"}, 201 | {"newMessageButton", "Uus sõnum"}, 202 | {"decodedMessage", "Dekrüpteeritud sõnum"}, 203 | {"messageRead", "Sõnum loetud, aegunud või link on vale"}, 204 | {"readMessageButton", "Dekrüpteeri sõnum"}, 205 | {"infoHeader", "kirjeldus"}, 206 | {"info", "Tööriist kasutab mitmesuguseid krüptimismeetodeid maksimaalse turvalisuse tagamiseks. " + 207 | "Linki olemasoluta ei saa sõnumit dekrüpteerida. Parooli kasutamine suurendab turvalisust veelgi. " + 208 | "Tööriista lähtekood on avatud ja seda saab vaadata"}, 209 | {"info1", ". Kui soovite meiega ühendust võtta, saatke meile"}, 210 | {"info2", "sõnum"}, 211 | {"info3", "."}, 212 | {"generalError", "Midagi läks valesti. Palun proovige hiljem uuesti."}, 213 | {"encryptNetworkError", "Midagi läks valesti. Ma ei saa sõnumit salvestada. Palun proovige uuesti."}, 214 | {"decryptNetworkError", "Midagi läks valesti. Ma ei saa sõnumit lugeda. Palun proovige uuesti."}, 215 | {"password", "Parool"}, 216 | {"enterPasswordPlaceholder", "sisestage parool"}, 217 | {"linkIsCorrupted", "Link on kahjustatud"}, 218 | }, 219 | language.Latvian: { 220 | {"title", "Privātas un drošas piezīmes - nosūtiet savas noslēpumus droši"}, 221 | {"header", "Privātas drošas piezīmes"}, 222 | {"description", "Drošas šifrētas ziņas"}, 223 | {"enterTextMessage", "Ievadiet šifrējamu ziņu"}, 224 | {"secureButton", "Šifrēt ziņu"}, 225 | {"copyLink", "Kopējiet saiti un nosūtiet to draugam. Ziņa tiks dzēsta nekavējoties pēc izlasīšanas vai pēc 4 nedēļām."}, 226 | {"copyLinkButton", "Kopēt saiti"}, 227 | {"newMessageButton", "Jauna ziņa"}, 228 | {"decodedMessage", "Atšifrēta ziņa"}, 229 | {"messageRead", "Ziņa izlasīta, beidzies derīguma termiņš vai saite ir nepareiza"}, 230 | {"readMessageButton", "Atšifrēt ziņu"}, 231 | {"infoHeader", "apraksts"}, 232 | {"info", "Rīks izmanto dažādas šifrēšanas metodes, lai nodrošinātu maksimālu drošību. " + 233 | "Bez saites ziņu nevar atšifrēt. Lai palielinātu drošību, izmantojiet paroli. " + 234 | "Rīka pirmkods ir atvērts un to var apskatīt vietnē"}, 235 | {"info1", ". Ja vēlaties sazināties ar mums, nosūtiet mums"}, 236 | {"info2", "ziņu"}, 237 | {"info3", "."}, 238 | {"generalError", "Kaut kas nogāja greizi. Lūdzu, mēģiniet vēlreiz vēlāk."}, 239 | {"encryptNetworkError", "Kaut kas nogāja greizi. Nevaru saglabāt ziņu. Lūdzu, mēģiniet vēlreiz."}, 240 | {"decryptNetworkError", "Kaut kas nogāja greizi. Nevaru nolasīt ziņu. Lūdzu, mēģiniet vēlreiz."}, 241 | {"password", "Parole"}, 242 | {"enterPasswordPlaceholder", "ievadiet paroli"}, 243 | {"linkIsCorrupted", "Saite ir bojāta"}, 244 | }, 245 | language.Lithuanian: { 246 | {"title", "Privatūs ir saugūs užrašai - siųskite savo paslaptis saugiai"}, 247 | {"header", "Privatūs saugūs užrašai"}, 248 | {"description", "Saugūs užšifruoti pranešimai"}, 249 | {"enterTextMessage", "Įveskite pranešimą, kurį norite užšifruoti"}, 250 | {"secureButton", "Užšifruoti pranešimą"}, 251 | {"copyLink", "Nukopijuokite nuorodą ir siųskite ją draugui. Pranešimas bus ištrintas iš karto po perskaitymo arba po 4 savaičių."}, 252 | {"copyLinkButton", "Kopijuoti nuorodą"}, 253 | {"newMessageButton", "Naujas pranešimas"}, 254 | {"decodedMessage", "Iššifruotas pranešimas"}, 255 | {"messageRead", "Pranešimas perskaitytas, pasibaigęs arba nuoroda neteisinga"}, 256 | {"readMessageButton", "Iššifruoti pranešimą"}, 257 | {"infoHeader", "aprašymas"}, 258 | {"info", "Įrankis naudoja įvairius šifravimo metodus, kad užtikrintų didžiausią saugumą. " + 259 | "Be nuorodos, pranešimo iššifruoti neįmanoma. Naudokite slaptažodį, kad padidintumėte saugumą. " + 260 | "Įrankio šaltinio kodas yra atviras ir gali būti peržiūrimas"}, 261 | {"info1", ". Jei norite susisiekti su mumis, siųskite mums"}, 262 | {"info2", "pranešimą"}, 263 | {"info3", "."}, 264 | {"generalError", "Kažkas nutiko. Prašome pabandyti vėliau."}, 265 | {"encryptNetworkError", "Kažkas nutiko. Negaliu išsaugoti pranešimo. Prašome pabandyti dar kartą."}, 266 | {"decryptNetworkError", "Kažkas nutiko. Negaliu perskaityti pranešimo. Prašome pabandyti dar kartą."}, 267 | {"password", "Slaptažodis"}, 268 | {"enterPasswordPlaceholder", "įveskite slaptažodį"}, 269 | {"linkIsCorrupted", "Nuoroda pažeista"}, 270 | }, 271 | language.Hungarian: { 272 | {"title", "Privát és biztonságos jegyzetek - küldje el titkait biztonságosan"}, 273 | {"header", "Privát biztonságos jegyzetek"}, 274 | {"description", "Biztonságos titkosított üzenetek"}, 275 | {"enterTextMessage", "Írja be a titkosítandó üzenetet"}, 276 | {"secureButton", "Üzenet titkosítása"}, 277 | {"copyLink", "Másolja a linket, és küldje el egy barátjának. Az üzenet azonnal törlődik az elolvasása után, vagy 4 hét elteltével."}, 278 | {"copyLinkButton", "Link másolása"}, 279 | {"newMessageButton", "Új üzenet"}, 280 | {"decodedMessage", "Dekódolt üzenet"}, 281 | {"messageRead", "Az üzenet elolvasásra került, lejárt, vagy a link hibás"}, 282 | {"readMessageButton", "Üzenet dekódolása"}, 283 | {"infoHeader", "leírás"}, 284 | {"info", "Az eszköz különböző titkosítási módszereket használ a maximális biztonság biztosítása érdekében. " + 285 | "A link nélkül az üzenetet nem lehet dekódolni. Használjon jelszót a biztonság további növelése érdekében. " + 286 | "Az eszköz forráskódja nyílt, és megtekinthető a"}, 287 | {"info1", ". Ha kapcsolatba szeretne lépni velünk, küldjön egy"}, 288 | {"info2", "üzenetet"}, 289 | {"info3", "."}, 290 | {"generalError", "Valami rosszul sikerült. Kérjük, próbálja meg később."}, 291 | {"encryptNetworkError", "Valami rosszul sikerült. Nem tudom menteni az üzenetet. Kérjük, próbálja meg újra."}, 292 | {"decryptNetworkError", "Valami rosszul sikerült. Nem tudom elolvasni az üzenetet. Kérjük, próbálja meg újra."}, 293 | {"password", "Jelszó"}, 294 | {"enterPasswordPlaceholder", "írja be a jelszót"}, 295 | {"linkIsCorrupted", "A link sérült"}, 296 | }, 297 | language.Polish: { 298 | {"title", "Prywatne bezpieczne wiadomości"}, 299 | {"header", "Prywatne wiadomości"}, 300 | {"description", "Bezpieczne szyfrowane wiadomości"}, 301 | {"enterTextMessage", "Wpisz wiadomość do zaszyfrowania"}, 302 | {"secureButton", "Szyfruj wiadomość"}, 303 | {"copyLink", "Skopiuj link i prześlij do przyjaciela. Wiadomość będzie skasowana natychmiast po odczytaniu lub po 4 tygodniach."}, 304 | {"copyLinkButton", "Skopiuj link"}, 305 | {"newMessageButton", "Nowa wiadomość"}, 306 | {"decodedMessage", "Odszyfrowana wiadomość"}, 307 | {"messageRead", "Wiadomość odczytana, przeterminowana lub link jest błędny"}, 308 | {"readMessageButton", "Odszyfruj wiadomość"}, 309 | {"infoHeader", "opis"}, 310 | {"info", "Narzędzie używa różnych metod szyfrowania, aby zapewnić maksymalne bezpieczeństwo. " + 311 | "Bez posiadania linku nie ma możliwości odszyfrowania wiadomości. Użyj hasła aby dodatkowo zwiększyć bezpieczeństwo. " + 312 | "Kod źródłowy narzędzia jest otwarty i możesz go obejrzeć w serwisie"}, 313 | {"info1", ". Jeśli chcesz się z nami skontaktować wyślij nam"}, 314 | {"info2", "wiadomość"}, 315 | {"info3", "."}, 316 | {"generalError", "Coś poszło nie tak. Spróbuj ponownie za jakiś czas."}, 317 | {"encryptNetworkError", "Coś poszło nie tak. Nie mogę zapisać wiadomości. Spróbuj ponownie."}, 318 | {"decryptNetworkError", "Coś poszło nie tak. Nie mogę odczytać wiadomości. Spróbuj ponownie."}, 319 | {"password", "Hasło"}, 320 | {"enterPasswordPlaceholder", "wprowadź hasło"}, 321 | {"linkIsCorrupted", "Link jest uszkodzony"}, 322 | }, 323 | language.Czech: { 324 | {"title", "Soukromé a zabezpečené poznámky - zašlete svá tajemství bezpečně"}, 325 | {"header", "Soukromé zabezpečené poznámky"}, 326 | {"description", "Zabezpečené šifrované zprávy"}, 327 | {"enterTextMessage", "Zadejte zprávu k zašifrování"}, 328 | {"secureButton", "Zašifrovat zprávu"}, 329 | {"copyLink", "Zkopírujte odkaz a pošlete jej příteli. Zpráva bude smazána ihned po přečtení nebo po 4 týdnech."}, 330 | {"copyLinkButton", "Kopírovat odkaz"}, 331 | {"newMessageButton", "Nová zpráva"}, 332 | {"decodedMessage", "Dešifrovaná zpráva"}, 333 | {"messageRead", "Zpráva byla přečtena, vypršela nebo je odkaz nesprávný"}, 334 | {"readMessageButton", "Dešifrovat zprávu"}, 335 | {"infoHeader", "popis"}, 336 | {"info", "Nástroj používá různé šifrovací metody pro zajištění maximálního zabezpečení. " + 337 | "Bez odkazu není možné zprávu dešifrovat. Použijte heslo pro další zvýšení zabezpečení. " + 338 | "Zdrojový kód nástroje je otevřený a lze jej zobrazit na"}, 339 | {"info1", ". Pokud nás chcete kontaktovat, pošlete nám"}, 340 | {"info2", "zprávu"}, 341 | {"info3", "."}, 342 | {"generalError", "Něco se pokazilo. Zkuste to prosím později."}, 343 | {"encryptNetworkError", "Něco se pokazilo. Nemohu uložit zprávu. Zkuste to znovu."}, 344 | {"decryptNetworkError", "Něco se pokazilo. Nemohu přečíst zprávu. Zkuste to znovu."}, 345 | {"password", "Heslo"}, 346 | {"enterPasswordPlaceholder", "zadejte heslo"}, 347 | {"linkIsCorrupted", "Odkaz je poškozen"}, 348 | }, 349 | language.Slovak: { 350 | {"title", "Súkromné a zabezpečené poznámky - bezpečne posielať svoje tajomstvá"}, 351 | {"header", "Súkromné zabezpečené poznámky"}, 352 | {"description", "Zabezpečené šifrované správy"}, 353 | {"enterTextMessage", "Zadajte správu na zašifrovanie"}, 354 | {"secureButton", "Zašifrovať správu"}, 355 | {"copyLink", "Skopírujte odkaz a pošlite ho priateľovi. Správa sa odstráni hneď po prečítaní alebo po 4 týždňoch."}, 356 | {"copyLinkButton", "Kopírovať odkaz"}, 357 | {"newMessageButton", "Nová správa"}, 358 | {"decodedMessage", "Dešifrovaná správa"}, 359 | {"messageRead", "Správa prečítaná, expirovaná alebo odkaz je nesprávny"}, 360 | {"readMessageButton", "Dešifrovať správu"}, 361 | {"infoHeader", "popis"}, 362 | {"info", "Nástroj používa rôzne šifrovacie metódy na zabezpečenie maximálnej ochrany. " + 363 | "Bez odkazu nie je možné dešifrovať správu. Použite heslo na ďalšie zvýšenie zabezpečenia. " + 364 | "Zdrojový kód nástroja je otvorený a môže byť prezretý na"}, 365 | {"info1", ". Ak nás chcete kontaktovať, pošlite nám"}, 366 | {"info2", "správu"}, 367 | {"info3", "."}, 368 | {"generalError", "Niečo sa pokazilo. Skúste to prosím neskôr."}, 369 | {"encryptNetworkError", "Niečo sa pokazilo. Nemôžem uložiť správu. Skúste to prosím neskôr."}, 370 | {"decryptNetworkError", "Niečo sa pokazilo. Nemôžem prečítať správu. Skúste to prosím neskôr."}, 371 | {"password", "Heslo"}, 372 | {"enterPasswordPlaceholder", "zadajte heslo"}, 373 | {"linkIsCorrupted", "Odkaz je poškodený"}, 374 | }, 375 | language.Slovenian: { 376 | {"title", "Zasebne in varne beležke - varno pošiljanje skrivnosti"}, 377 | {"header", "Zasebne varne beležke"}, 378 | {"description", "Varno šifrirana sporočila"}, 379 | {"enterTextMessage", "Vnesite sporočilo za šifriranje"}, 380 | {"secureButton", "Šifrirajte sporočilo"}, 381 | {"copyLink", "Kopirajte povezavo in jo pošljite prijatelju. Sporočilo bo izbrisano takoj po branju ali po 4 tednih."}, 382 | {"copyLinkButton", "Kopiraj povezavo"}, 383 | {"newMessageButton", "Novo sporočilo"}, 384 | {"decodedMessage", "Dešifrirano sporočilo"}, 385 | {"messageRead", "Sporočilo je prebrano, poteklo ali pa je povezava napačna"}, 386 | {"readMessageButton", "Dešifriraj sporočilo"}, 387 | {"infoHeader", "opis"}, 388 | {"info", "Orodje uporablja različne šifrirne metode za zagotavljanje največje varnosti. " + 389 | "Brez povezave ni mogoče dešifrirati sporočila. Uporabite geslo za dodatno povečanje varnosti. " + 390 | "Izvorna koda orodja je odprta in si jo je mogoče ogledati na"}, 391 | {"info1", ". Če nas želite kontaktirati, nam pošljite"}, 392 | {"info2", "sporočilo"}, 393 | {"info3", "."}, 394 | {"generalError", "Nekaj je šlo narobe. Poskusite znova kasneje."}, 395 | {"encryptNetworkError", "Nekaj je šlo narobe. Ne morem shraniti sporočila. Poskusite znova."}, 396 | {"decryptNetworkError", "Nekaj je šlo narobe. Ne morem prebrati sporočila. Poskusite znova."}, 397 | {"password", "Geslo"}, 398 | {"enterPasswordPlaceholder", "vnesite geslo"}, 399 | {"linkIsCorrupted", "Povezava je poškodovana"}, 400 | }, 401 | language.Spanish: { 402 | {"title", "Notas privadas y seguras: envía tus secretos de forma segura"}, 403 | {"header", "Notas privadas seguras"}, 404 | {"description", "Mensajes seguros cifrados"}, 405 | {"enterTextMessage", "Introduce el mensaje para cifrar"}, 406 | {"secureButton", "Cifrar el mensaje"}, 407 | {"copyLink", "Copia el enlace y envíalo a un amigo. El mensaje se eliminará inmediatamente después de ser leído o después de 4 semanas."}, 408 | {"copyLinkButton", "Copiar enlace"}, 409 | {"newMessageButton", "Nuevo mensaje"}, 410 | {"decodedMessage", "Mensaje descifrado"}, 411 | {"messageRead", "Mensaje leído, caducado o el enlace es incorrecto"}, 412 | {"readMessageButton", "Descifrar mensaje"}, 413 | {"infoHeader", "descripción"}, 414 | {"info", "La herramienta utiliza varios métodos de cifrado para garantizar la máxima seguridad. " + 415 | "Sin el enlace, no es posible descifrar el mensaje. Utiliza una contraseña para aumentar aún más la seguridad. " + 416 | "El código fuente de la herramienta es abierto y se puede ver en"}, 417 | {"info1", ". Si quieres ponerte en contacto con nosotros, envíanos un"}, 418 | {"info2", "mensaje"}, 419 | {"info3", "."}, 420 | {"generalError", "Algo salió mal. Por favor, inténtalo de nuevo más tarde."}, 421 | {"encryptNetworkError", "Algo salió mal. No puedo guardar el mensaje. Por favor, inténtalo de nuevo."}, 422 | {"decryptNetworkError", "Algo salió mal. No puedo leer el mensaje. Por favor, inténtalo de nuevo."}, 423 | {"password", "Contraseña"}, 424 | {"enterPasswordPlaceholder", "introduce la contraseña"}, 425 | {"linkIsCorrupted", "El enlace está dañado"}, 426 | }, 427 | language.Portuguese: { 428 | {"title", "Notas privadas e seguras - envie seus segredos com segurança"}, 429 | {"header", "Notas privadas seguras"}, 430 | {"description", "Mensagens seguras e criptografadas"}, 431 | {"enterTextMessage", "Digite a mensagem para criptografar"}, 432 | {"secureButton", "Criptografar a mensagem"}, 433 | {"copyLink", "Copie o link e envie para um amigo. A mensagem será excluída imediatamente após ser lida ou após 4 semanas."}, 434 | {"copyLinkButton", "Copiar link"}, 435 | {"newMessageButton", "Nova mensagem"}, 436 | {"decodedMessage", "Mensagem descriptografada"}, 437 | {"messageRead", "Mensagem lida, expirada ou o link está incorreto"}, 438 | {"readMessageButton", "Descriptografar mensagem"}, 439 | {"infoHeader", "descrição"}, 440 | {"info", "A ferramenta utiliza vários métodos de criptografia para garantir a máxima segurança. " + 441 | "Sem o link, não é possível descriptografar a mensagem. Use uma senha para aumentar ainda mais a segurança. " + 442 | "O código-fonte da ferramenta é aberto e pode ser visualizado no"}, 443 | {"info1", ". Se você deseja entrar em contato conosco, envie-nos uma"}, 444 | {"info2", "mensagem"}, 445 | {"info3", "."}, 446 | {"generalError", "Algo deu errado. Por favor, tente novamente mais tarde."}, 447 | {"encryptNetworkError", "Algo deu errado. Não consigo salvar a mensagem. Por favor, tente novamente."}, 448 | {"decryptNetworkError", "Algo deu errado. Não consigo ler a mensagem. Por favor, tente novamente."}, 449 | {"password", "Senha"}, 450 | {"enterPasswordPlaceholder", "insira a senha"}, 451 | {"linkIsCorrupted", "O link está corrompido"}, 452 | }, 453 | language.Italian: { 454 | {"title", "Note private e sicure: invia i tuoi segreti in tutta sicurezza"}, 455 | {"header", "Note private e sicure"}, 456 | {"description", "Messaggi sicuri e criptati"}, 457 | {"enterTextMessage", "Inserisci il messaggio da criptare"}, 458 | {"secureButton", "Cripta il messaggio"}, 459 | {"copyLink", "Copia il link e invialo a un amico. Il messaggio verrà eliminato immediatamente dopo essere stato letto o dopo 4 settimane."}, 460 | {"copyLinkButton", "Copia il link"}, 461 | {"newMessageButton", "Nuovo messaggio"}, 462 | {"decodedMessage", "Messaggio decriptato"}, 463 | {"messageRead", "Messaggio letto, scaduto o il link è errato"}, 464 | {"readMessageButton", "Decripta il messaggio"}, 465 | {"infoHeader", "descrizione"}, 466 | {"info", "Lo strumento utilizza vari metodi di crittografia per garantire la massima sicurezza. " + 467 | "Senza il link, non è possibile decriptare il messaggio. Usa una password per aumentare ulteriormente la sicurezza. " + 468 | "Il codice sorgente dello strumento è aperto e può essere visualizzato su"}, 469 | {"info1", ". Se vuoi contattarci, inviaci un"}, 470 | {"info2", "messaggio"}, 471 | {"info3", "."}, 472 | {"generalError", "Qualcosa è andato storto. Riprova più tardi."}, 473 | {"encryptNetworkError", "Qualcosa è andato storto. Non riesco a salvare il messaggio. Riprova."}, 474 | {"decryptNetworkError", "Qualcosa è andato storto. Non riesco a leggere il messaggio. Riprova."}, 475 | {"password", "Password"}, 476 | {"enterPasswordPlaceholder", "inserisci la password"}, 477 | {"linkIsCorrupted", "Il link è danneggiato"}, 478 | }, 479 | language.Romanian: { 480 | {"title", "Notițe private și securizate - trimiteți secretele în siguranță"}, 481 | {"header", "Notițe securizate private"}, 482 | {"description", "Mesaje criptate securizate"}, 483 | {"enterTextMessage", "Introduceți mesajul de criptat"}, 484 | {"secureButton", "Criptează mesajul"}, 485 | {"copyLink", "Copiați linkul și trimiteți-l unui prieten. Mesajul va fi șters imediat după ce este citit sau după 4 săptămâni."}, 486 | {"copyLinkButton", "Copiați linkul"}, 487 | {"newMessageButton", "Mesaj nou"}, 488 | {"decodedMessage", "Mesaj decriptat"}, 489 | {"messageRead", "Mesaj citit, expirat sau link incorect"}, 490 | {"readMessageButton", "Decriptează mesajul"}, 491 | {"infoHeader", "descriere"}, 492 | {"info", "Instrumentul utilizează diferite metode de criptare pentru a asigura securitatea maximă. " + 493 | "Fără link, nu este posibil să decriptați mesajul. Utilizați o parolă pentru a crește și mai mult securitatea. " + 494 | "Codul sursă al instrumentului este deschis și poate fi vizualizat pe"}, 495 | {"info1", ". Dacă doriți să ne contactați, trimiteți-ne un"}, 496 | {"info2", "mesaj"}, 497 | {"info3", "."}, 498 | {"generalError", "Ceva nu a mers bine. Vă rugăm să încercați din nou mai târziu."}, 499 | {"encryptNetworkError", "Ceva nu a mers bine. Nu pot salva mesajul. Vă rugăm să încercați din nou."}, 500 | {"decryptNetworkError", "Ceva nu a mers bine. Nu pot citi mesajul. Vă rugăm să încercați din nou."}, 501 | {"password", "Parolă"}, 502 | {"enterPasswordPlaceholder", "introduceți parola"}, 503 | {"linkIsCorrupted", "Link-ul este corupt"}, 504 | }, 505 | language.French: { 506 | {"title", "Notes privées et sécurisées - envoyez vos secrets en toute sécurité"}, 507 | {"header", "Notes privées sécurisées"}, 508 | {"description", "Messages sécurisés et cryptés"}, 509 | {"enterTextMessage", "Entrez le message à crypter"}, 510 | {"secureButton", "Crypter le message"}, 511 | {"copyLink", "Copiez le lien et envoyez-le à un ami. Le message sera supprimé immédiatement après avoir été lu ou après 4 semaines."}, 512 | {"copyLinkButton", "Copier le lien"}, 513 | {"newMessageButton", "Nouveau message"}, 514 | {"decodedMessage", "Message déchiffré"}, 515 | {"messageRead", "Message lu, expiré ou le lien est incorrect"}, 516 | {"readMessageButton", "Déchiffrer le message"}, 517 | {"infoHeader", "description"}, 518 | {"info", "L'outil utilise diverses méthodes de cryptage pour assurer une sécurité maximale. " + 519 | "Sans le lien, il est impossible de déchiffrer le message. Utilisez un mot de passe pour renforcer davantage la sécurité. " + 520 | "Le code source de l'outil est ouvert et peut être consulté sur"}, 521 | {"info1", ". Si vous souhaitez nous contacter, envoyez-nous un"}, 522 | {"info2", "message"}, 523 | {"info3", "."}, 524 | {"generalError", "Un problème est survenu. Veuillez réessayer plus tard."}, 525 | {"encryptNetworkError", "Un problème est survenu. Je ne peux pas enregistrer le message. Veuillez réessayer."}, 526 | {"decryptNetworkError", "Un problème est survenu. Je ne peux pas lire le message. Veuillez réessayer."}, 527 | {"password", "Mot de passe"}, 528 | {"enterPasswordPlaceholder", "entrez le mot de passe"}, 529 | {"linkIsCorrupted", "Le lien est endommagé"}, 530 | }, 531 | language.Ukrainian: { 532 | {"title", "Приватні та безпечні нотатки - безпечна передача секретів"}, 533 | {"header", "Приватні безпечні нотатки"}, 534 | {"description", "Безпечні зашифровані повідомлення"}, 535 | {"enterTextMessage", "Введіть повідомлення для шифрування"}, 536 | {"secureButton", "Зашифрувати повідомлення"}, 537 | {"copyLink", "Скопіюйте посилання та надішліть його другу. Повідомлення буде видалено одразу після прочитання або через 4 тижні."}, 538 | {"copyLinkButton", "Копіювати посилання"}, 539 | {"newMessageButton", "Нове повідомлення"}, 540 | {"decodedMessage", "Розшифроване повідомлення"}, 541 | {"messageRead", "Повідомлення прочитано, прострочено або посилання невірне"}, 542 | {"readMessageButton", "Розшифрувати повідомлення"}, 543 | {"infoHeader", "опис"}, 544 | {"info", "Інструмент використовує різні методи шифрування для забезпечення максимальної безпеки. " + 545 | "Без посилання неможливо розшифрувати повідомлення. Використовуйте пароль для додаткового збільшення безпеки. " + 546 | "Вихідний код інструменту відкритий та доступний для перегляду на"}, 547 | {"info1", ". Якщо ви хочете зв'язатися з нами, надішліть нам"}, 548 | {"info2", "повідомлення"}, 549 | {"info3", "."}, 550 | {"generalError", "Щось пішло не так. Будь ласка, спробуйте знову пізніше."}, 551 | {"encryptNetworkError", "Щось пішло не так. Я не можу зберегти повідомлення. Будь ласка, спробуйте знову."}, 552 | {"decryptNetworkError", "Щось пішло не так. Я не можу прочитати повідомлення. Будь ласка, спробуйте знову."}, 553 | {"password", "Пароль"}, 554 | {"enterPasswordPlaceholder", "введіть пароль"}, 555 | {"linkIsCorrupted", "Посилання пошкоджено"}, 556 | }, 557 | language.Russian: { 558 | {"title", "Приватные и безопасные заметки - безопасная передача секретов"}, 559 | {"header", "Приватные безопасные заметки"}, 560 | {"description", "Безопасные зашифрованные сообщения"}, 561 | {"enterTextMessage", "Введите сообщение для шифрования"}, 562 | {"secureButton", "Зашифровать сообщение"}, 563 | {"copyLink", "Скопируйте ссылку и отправьте ее другу. Сообщение будет удалено сразу после прочтения или через 4 недели."}, 564 | {"copyLinkButton", "Копировать ссылку"}, 565 | {"newMessageButton", "Новое сообщение"}, 566 | {"decodedMessage", "Расшифрованное сообщение"}, 567 | {"messageRead", "Сообщение прочитано, истекло или ссылка неверна"}, 568 | {"readMessageButton", "Расшифровать сообщение"}, 569 | {"infoHeader", "описание"}, 570 | {"info", "Инструмент использует различные методы шифрования для обеспечения максимальной безопасности. " + 571 | "Без ссылки невозможно расшифровать сообщение. Используйте пароль для дополнительного увеличения безопасности. " + 572 | "Исходный код инструмента открыт и может быть просмотрен на"}, 573 | {"info1", ". Если вы хотите связаться с нами, отправьте нам"}, 574 | {"info2", "сообщение"}, 575 | {"info3", "."}, 576 | {"generalError", "Что-то пошло не так. Пожалуйста, попробуйте еще раз позже."}, 577 | {"encryptNetworkError", "Что-то пошло не так. Я не могу сохранить сообщение. Пожалуйста, попробуйте еще раз."}, 578 | {"decryptNetworkError", "Что-то пошло не так. Я не могу прочитать сообщение. Пожалуйста, попробуйте еще раз."}, 579 | {"password", "Пароль"}, 580 | {"enterPasswordPlaceholder", "введите пароль"}, 581 | {"linkIsCorrupted", "Ссылка повреждена"}, 582 | }, 583 | language.Bulgarian: { 584 | {"title", "Частни и сигурни бележки - изпращайте тайните си безопасно"}, 585 | {"header", "Частни сигурни бележки"}, 586 | {"description", "Сигурни криптирани съобщения"}, 587 | {"enterTextMessage", "Въведете съобщението за криптиране"}, 588 | {"secureButton", "Криптирайте съобщението"}, 589 | {"copyLink", "Копирайте връзката и я изпратете на приятел. Съобщението ще бъде изтрито веднага след прочитането му или след 4 седмици."}, 590 | {"copyLinkButton", "Копирай връзката"}, 591 | {"newMessageButton", "Ново съобщение"}, 592 | {"decodedMessage", "Декриптирано съобщение"}, 593 | {"messageRead", "Съобщението е прочетено, изтекло или връзката е некоректна"}, 594 | {"readMessageButton", "Декриптирай съобщението"}, 595 | {"infoHeader", "описание"}, 596 | {"info", "Инструментът използва различни методи за криптиране, за да гарантира максимална сигурност. " + 597 | "Без връзката не е възможно да се декриптира съобщението. Използвайте парола, за да увеличите сигурността. " + 598 | "Изходният код на инструмента е отворен и може да се види на"}, 599 | {"info1", ". Ако искате да се свържете с нас, изпратете ни"}, 600 | {"info2", "съобщение"}, 601 | {"info3", "."}, 602 | {"generalError", "Нещо се обърка. Моля, опитайте отново по-късно."}, 603 | {"encryptNetworkError", "Нещо се обърка. Не мога да запазя съобщението. Моля, опитайте отново."}, 604 | {"decryptNetworkError", "Нещо се обърка. Не мога да прочета съобщението. Моля, опитайте отново."}, 605 | {"password", "Парола"}, 606 | {"enterPasswordPlaceholder", "въведете паролата"}, 607 | {"linkIsCorrupted", "Връзката е повредена"}, 608 | }, 609 | language.Croatian: { 610 | {"title", "Privatne i sigurne bilješke - sigurno šaljite svoje tajne"}, 611 | {"header", "Privatne sigurne bilješke"}, 612 | {"description", "Sigurne kriptirane poruke"}, 613 | {"enterTextMessage", "Unesite poruku za šifriranje"}, 614 | {"secureButton", "Šifrirajte poruku"}, 615 | {"copyLink", "Kopirajte poveznicu i pošaljite je prijatelju. Poruka će biti izbrisana odmah nakon što se pročita ili nakon 4 tjedna."}, 616 | {"copyLinkButton", "Kopiraj poveznicu"}, 617 | {"newMessageButton", "Nova poruka"}, 618 | {"decodedMessage", "Dešifrirana poruka"}, 619 | {"messageRead", "Poruka pročitana, istekla ili poveznica nije točna"}, 620 | {"readMessageButton", "Dešifriraj poruku"}, 621 | {"infoHeader", "opis"}, 622 | {"info", "Alat koristi razne metode šifriranja kako bi osigurao maksimalnu sigurnost. " + 623 | "Bez poveznice nije moguće dešifrirati poruku. Koristite lozinku kako biste dodatno povećali sigurnost. " + 624 | "Izvorni kod alata je otvoren i može se pregledati na"}, 625 | {"info1", ". Ako želite stupiti u kontakt s nama, pošaljite nam"}, 626 | {"info2", "poruku"}, 627 | {"info3", "."}, 628 | {"generalError", "Nešto je pošlo po zlu. Molimo pokušajte ponovno kasnije."}, 629 | {"encryptNetworkError", "Nešto je pošlo po zlu. Ne mogu spremiti poruku. Molimo pokušajte ponovno."}, 630 | {"decryptNetworkError", "Nešto je pošlo po zlu. Ne mogu pročitati poruku. Molimo pokušajte ponovno."}, 631 | {"password", "Lozinka"}, 632 | {"enterPasswordPlaceholder", "unesite lozinku"}, 633 | {"linkIsCorrupted", "Poveznica je oštećena"}, 634 | }, 635 | language.Albanian: { 636 | {"title", "Shënime private dhe të sigurta - dërgo sekretet tënde me siguri"}, 637 | {"header", "Shënime private të sigurta"}, 638 | {"description", "Mesazhe të koduara të sigurta"}, 639 | {"enterTextMessage", "Shkruaj mesazhin për t'u koduar"}, 640 | {"secureButton", "Kodoni mesazhin"}, 641 | {"copyLink", "Kopjoni lidhjen dhe dërgojani atë një mikut. Mesazhi do të fshihet menjëherë pasi të lexohet ose pas 4 javësh."}, 642 | {"copyLinkButton", "Kopjo lidhjen"}, 643 | {"newMessageButton", "Mesazh i ri"}, 644 | {"decodedMessage", "Mesazh i dekoduar"}, 645 | {"messageRead", "Mesazhi është lexuar, ka skaduar, ose lidhja është e pasaktë"}, 646 | {"readMessageButton", "Dekodoni mesazhin"}, 647 | {"infoHeader", "përshkrim"}, 648 | {"info", "Mjeti përdor metoda të ndryshme kodimi për të siguruar maksimumin e sigurisë. " + 649 | "Pa lidhjen, nuk është e mundur të dekodohet mesazhi. Përdorni një fjalëkalim për të rritur sigurinë më tej. " + 650 | "Kodi burimor i mjetit është i hapur dhe mund të shikohet në"}, 651 | {"info1", ". Nëse dëshironi të na kontaktoni, dërgoni një"}, 652 | {"info2", "mesazh"}, 653 | {"info3", "."}, 654 | {"generalError", "Diçka shkoi keq. Ju lutemi provoni përsëri më vonë."}, 655 | {"encryptNetworkError", "Diçka shkoi keq. Nuk mund të ruaj mesazhin. Ju lutemi provoni përsëri."}, 656 | {"decryptNetworkError", "Diçka shkoi keq. Nuk mund të lexoj mesazhin. Ju lutemi provoni përsëri."}, 657 | {"password", "Fjalëkalimi"}, 658 | {"enterPasswordPlaceholder", "shkruani fjalëkalimin"}, 659 | {"linkIsCorrupted", "Lidhja është e dëmtuar"}, 660 | }, 661 | language.Serbian: { 662 | {"title", "Privatne i sigurne beleške - bezbedno šaljite svoje tajne"}, 663 | {"header", "Privatne sigurne beleške"}, 664 | {"description", "Sigurne šifrovane poruke"}, 665 | {"enterTextMessage", "Unesite poruku za šifrovanje"}, 666 | {"secureButton", "Šifruj poruku"}, 667 | {"copyLink", "Kopirajte link i pošaljite ga prijatelju. Poruka će biti odmah obrisana nakon čitanja ili nakon 4 nedelje."}, 668 | {"copyLinkButton", "Kopiraj link"}, 669 | {"newMessageButton", "Nova poruka"}, 670 | {"decodedMessage", "Dešifrovana poruka"}, 671 | {"messageRead", "Poruka pročitana, istekla ili je link netačan"}, 672 | {"readMessageButton", "Dešifruj poruku"}, 673 | {"infoHeader", "opis"}, 674 | {"info", "Alat koristi različite metode šifrovanja kako bi osigurao maksimalnu sigurnost. " + 675 | "Bez linka, nije moguće dešifrovati poruku. Koristite lozinku kako biste dodatno povećali sigurnost. " + 676 | "Izvorni kod alata je otvoren i može se pogledati na"}, 677 | {"info1", ". Ako želite da nas kontaktirate, pošaljite nam"}, 678 | {"info2", "poruku"}, 679 | {"info3", "."}, 680 | {"generalError", "Došlo je do greške. Molimo pokušajte ponovo kasnije."}, 681 | {"encryptNetworkError", "Došlo je do greške. Ne mogu sačuvati poruku. Molimo pokušajte ponovo."}, 682 | {"decryptNetworkError", "Došlo je do greške. Ne mogu pročitati poruku. Molimo pokušajte ponovo."}, 683 | {"password", "Lozinka"}, 684 | {"enterPasswordPlaceholder", "unesite lozinku"}, 685 | {"linkIsCorrupted", "Link je oštećen"}, 686 | }, 687 | language.Greek: { 688 | {"title", "Ιδιωτικές και ασφαλείς σημειώσεις - στείλτε τα μυστικά σας με ασφάλεια"}, 689 | {"header", "Ιδιωτικές ασφαλείς σημειώσεις"}, 690 | {"description", "Ασφαλή κρυπτογραφημένα μηνύματα"}, 691 | {"enterTextMessage", "Εισάγετε το μήνυμα προς κρυπτογράφηση"}, 692 | {"secureButton", "Κρυπτογράφηση του μηνύματος"}, 693 | {"copyLink", "Αντιγράψτε τον σύνδεσμο και στείλτε τον σε ένα φίλο. Το μήνυμα θα διαγραφεί αμέσως μετά την ανάγνωση ή μετά από 4 εβδομάδες."}, 694 | {"copyLinkButton", "Αντιγραφή συνδέσμου"}, 695 | {"newMessageButton", "Νέο μήνυμα"}, 696 | {"decodedMessage", "Αποκρυπτογραφημένο μήνυμα"}, 697 | {"messageRead", "Το μήνυμα διαβάστηκε, έληξε ή ο σύνδεσμος είναι λανθασμένος"}, 698 | {"readMessageButton", "Αποκρυπτογράφηση μηνύματος"}, 699 | {"infoHeader", "περιγραφή"}, 700 | {"info", "Το εργαλείο χρησιμοποιεί διάφορες μεθόδους κρυπτογράφησης για να εξασφαλίσει τη μέγιστη ασφάλεια. " + 701 | "Χωρίς τον σύνδεσμο, δεν είναι δυνατή η αποκρυπτογράφηση του μηνύματος. Χρησιμοποιήστε έναν κωδικό πρόσβασης για να αυξήσετε ακόμα περισσότερο την ασφάλεια. " + 702 | "Ο πηγαίος κώδικας του εργαλείου είναι ανοιχτός και μπορεί να προβληθεί στο"}, 703 | {"info1", ". Εάν θέλετε να επικοινωνήσετε μαζί μας, στείλτε μας ένα"}, 704 | {"info2", "μήνυμα"}, 705 | {"info3", "."}, 706 | {"generalError", "Κάτι πήγε στραβά. Παρακαλώ δοκιμάστε ξανά αργότερα."}, 707 | {"encryptNetworkError", "Κάτι πήγε στραβά. Δεν μπορώ να αποθηκεύσω το μήνυμα. Παρακαλώ δοκιμάστε ξανά."}, 708 | {"decryptNetworkError", "Κάτι πήγε στραβά. Δεν μπορώ να διαβάσω το μήνυμα. Παρακαλώ δοκιμάστε ξανά."}, 709 | {"password", "Κωδικός πρόσβασης"}, 710 | {"enterPasswordPlaceholder", "εισάγετε τον κωδικό πρόσβασης"}, 711 | {"linkIsCorrupted", "Ο σύνδεσμος είναι κατεστραμμένος"}, 712 | }, 713 | language.SimplifiedChinese: { 714 | {"title", "私密且安全的笔记 - 安全发送您的秘密"}, 715 | {"header", "私密安全笔记"}, 716 | {"description", "安全加密的消息"}, 717 | {"enterTextMessage", "输入要加密的消息"}, 718 | {"secureButton", "加密消息"}, 719 | {"copyLink", "复制链接并发送给朋友。消息将在被阅读后或 4 周后立即删除。"}, 720 | {"copyLinkButton", "复制链接"}, 721 | {"newMessageButton", "新消息"}, 722 | {"decodedMessage", "解密的消息"}, 723 | {"messageRead", "消息已读、过期或链接错误"}, 724 | {"readMessageButton", "解密消息"}, 725 | {"infoHeader", "描述"}, 726 | {"info", "该工具使用各种加密方法确保最大程度的安全性。" + 727 | "没有链接,就无法解密消息。使用密码进一步提高安全性。" + 728 | "该工具的源代码是开放的,可以在"}, 729 | {"info1", "上查看。如果您想联系我们,请给我们发送一条"}, 730 | {"info2", "消息"}, 731 | {"info3", "。"}, 732 | {"generalError", "出了点问题。请稍后再试。"}, 733 | {"encryptNetworkError", "出了点问题。无法保存消息。请重试。"}, 734 | {"decryptNetworkError", "出了点问题。无法读取消息。请重试。"}, 735 | {"password", "密码"}, 736 | {"enterPasswordPlaceholder", "输入密码"}, 737 | {"linkIsCorrupted", "链接已损坏"}, 738 | }, 739 | language.Indonesian: { 740 | {"title", "Catatan pribadi dan aman - kirim rahasia Anda dengan aman"}, 741 | {"header", "Catatan pribadi yang aman"}, 742 | {"description", "Pesan terenkripsi aman"}, 743 | {"enterTextMessage", "Masukkan pesan untuk mengenkripsi"}, 744 | {"secureButton", "Enkripsi pesan"}, 745 | {"copyLink", "Salin tautan dan kirim ke teman. Pesan akan dihapus segera setelah dibaca atau setelah 4 minggu."}, 746 | {"copyLinkButton", "Salin tautan"}, 747 | {"newMessageButton", "Pesan baru"}, 748 | {"decodedMessage", "Pesan terdekripsi"}, 749 | {"messageRead", "Pesan telah dibaca, kadaluwarsa, atau tautannya salah"}, 750 | {"readMessageButton", "Dekripsi pesan"}, 751 | {"infoHeader", "deskripsi"}, 752 | {"info", "Alat ini menggunakan berbagai metode enkripsi untuk memastikan keamanan maksimal. " + 753 | "Tanpa tautan, tidak mungkin untuk mendekripsi pesan. Gunakan kata sandi untuk meningkatkan keamanan lebih lanjut. " + 754 | "Kode sumber alat ini terbuka dan dapat dilihat di"}, 755 | {"info1", ". Jika Anda ingin menghubungi kami, kirimkan"}, 756 | {"info2", "pesan"}, 757 | {"info3", "."}, 758 | {"generalError", "Ada yang salah. Silakan coba lagi nanti."}, 759 | {"encryptNetworkError", "Ada yang salah. Saya tidak dapat menyimpan pesan. Silakan coba lagi."}, 760 | {"decryptNetworkError", "Ada yang salah. Saya tidak dapat membaca pesan. Silakan coba lagi."}, 761 | {"password", "Kata sandi"}, 762 | {"enterPasswordPlaceholder", "masukkan kata sandi"}, 763 | {"linkIsCorrupted", "Tautan rusak"}, 764 | }, 765 | language.Turkish: { 766 | {"title", "Özel ve güvenli notlar - sırlarınızı güvenle gönderin"}, 767 | {"header", "Özel güvenli notlar"}, 768 | {"description", "Güvenli şifreli mesajlar"}, 769 | {"enterTextMessage", "Şifrelemek için mesajı girin"}, 770 | {"secureButton", "Mesajı şifrele"}, 771 | {"copyLink", "Bağlantıyı kopyalayın ve bir arkadaşa gönderin. Mesaj, okunduktan hemen sonra veya 4 hafta sonra silinecektir."}, 772 | {"copyLinkButton", "Bağlantıyı kopyala"}, 773 | {"newMessageButton", "Yeni mesaj"}, 774 | {"decodedMessage", "Şifresi çözülmüş mesaj"}, 775 | {"messageRead", "Mesaj okundu, süresi doldu veya bağlantı yanlış"}, 776 | {"readMessageButton", "Mesajın şifresini çöz"}, 777 | {"infoHeader", "açıklama"}, 778 | {"info", "Araç, maksimum güvenliği sağlamak için çeşitli şifreleme yöntemleri kullanır. " + 779 | "Bağlantı olmadan, mesajın şifresini çözmek mümkün değildir. Güvenliği daha da artırmak için bir şifre kullanın. " + 780 | "Araçın kaynak kodu açıktır ve"}, 781 | {"info1", "'da görüntülenebilir. Bize ulaşmak isterseniz, bize bir"}, 782 | {"info2", "mesaj"}, 783 | {"info3", "."}, 784 | {"generalError", "Bir şeyler yanlış gitti. Lütfen daha sonra tekrar deneyin."}, 785 | {"encryptNetworkError", "Bir şeyler yanlış gitti. Mesajı kaydedemiyorum. Lütfen tekrar deneyin."}, 786 | {"decryptNetworkError", "Bir şeyler yanlış gitti. Mesajı okuyamıyorum. Lütfen tekrar deneyin."}, 787 | {"password", "Parola"}, 788 | {"enterPasswordPlaceholder", "parolayı girin"}, 789 | {"linkIsCorrupted", "Bağlantı bozuk"}, 790 | }, 791 | language.Hindi: { 792 | {"title", "निजी और सुरक्षित नोट्स - अपने रहस्यों को सुरक्षित रूप से भेजें"}, 793 | {"header", "निजी सुरक्षित नोट्स"}, 794 | {"description", "सुरक्षित एन्क्रिप्टेड संदेश"}, 795 | {"enterTextMessage", "एन्क्रिप्ट करने के लिए संदेश दर्ज करें"}, 796 | {"secureButton", "संदेश को एन्क्रिप्ट करें"}, 797 | {"copyLink", "लिंक को कॉपी करें और इसे एक मित्र को भेजें। संदेश पढ़ने के तुरंत बाद या 4 सप्ताह के बाद हटा दिया जाएगा।"}, 798 | {"copyLinkButton", "लिंक कॉपी करें"}, 799 | {"newMessageButton", "नया संदेश"}, 800 | {"decodedMessage", "डिक्रिप्टेड संदेश"}, 801 | {"messageRead", "संदेश पढ़ा गया, समाप्त हो गया, या लिंक गलत है"}, 802 | {"readMessageButton", "संदेश डिक्रिप्ट करें"}, 803 | {"infoHeader", "विवरण"}, 804 | {"info", "यह उपकरण अधिकतम सुरक्षा सुनिश्चित करने के लिए विभिन्न एन्क्रिप्शन तरीकों का उपयोग करता है। " + 805 | "लिंक के बिना, संदेश को डिक्रिप्ट करना संभव नहीं है। सुरक्षा में और वृद्धि के लिए पासवर्ड का उपयोग करें। " + 806 | "उपकरण का स्रोत कोड खुला है और"}, 807 | {"info1", "पर देखा जा सकता है। यदि आप हमसे संपर्क करना चाहते हैं, तो हमें एक"}, 808 | {"info2", "संदेश"}, 809 | {"info3", "भेजें।"}, 810 | {"generalError", "कुछ गलत हो गया। कृपया बाद में पुनः प्रयास करें।"}, 811 | {"encryptNetworkError", "कुछ गलत हो गया। मैं संदेश को सहेज नहीं सकता। कृपया पुनः प्रयास करें।"}, 812 | {"decryptNetworkError", "कुछ गलत हो गया। मैं संदेश को पढ़ नहीं सकता। कृपया पुनः प्रयास करें।"}, 813 | {"password", "पासवर्ड"}, 814 | {"enterPasswordPlaceholder", "पासवर्ड दर्ज करें"}, 815 | {"linkIsCorrupted", "लिंक क्षतिग्रस्त है"}, 816 | }, 817 | language.Bengali: { 818 | {"title", "ব্যক্তিগত এবং নিরাপদ নোটস - আপনার গোপন তথ্য নিরাপদে পাঠান"}, 819 | {"header", "ব্যক্তিগত নিরাপদ নোটস"}, 820 | {"description", "নিরাপদ এনক্রিপ্টেড বার্তা"}, 821 | {"enterTextMessage", "এনক্রিপ্ট করার জন্য বার্তা লিখুন"}, 822 | {"secureButton", "বার্তাটি এনক্রিপ্ট করুন"}, 823 | {"copyLink", "লিঙ্কটি কপি করুন এবং এটি একটি বন্ধুর কাছে পাঠান। বার্তাটি পড়ার পরে অথবা ৪ সপ্তাহের মধ্যে মুছে ফেলা হবে।"}, 824 | {"copyLinkButton", "লিঙ্ক কপি করুন"}, 825 | {"newMessageButton", "নতুন বার্তা"}, 826 | {"decodedMessage", "ডিক্রিপ্টেড বার্তা"}, 827 | {"messageRead", "বার্তা পড়া হয়েছে, মেয়াদ শেষ হয়েছে, অথবা লিঙ্কটি ভুল"}, 828 | {"readMessageButton", "বার্তা ডিক্রিপ্ট করুন"}, 829 | {"infoHeader", "বিবরণ"}, 830 | {"info", "এই সরঞ্জামটি বিভিন্ন এনক্রিপশন পদ্ধতি ব্যবহার করে সর্বাধিক নিরাপত্তি নিশ্চিত করে। " + 831 | "লিঙ্ক ছাড়া, বার্তাটি ডিক্রিপ্ট করা সম্ভব নয়। নিরাপত্তি বাড়ানোর জন্য পাসওয়ার্ড ব্যবহার করুন। " + 832 | "সরঞ্জামটির উৎস কোড খোলা আছে এবং"}, 833 | {"info1", "দেখা যেতে পারে। আপনি যদি আমাদের সাথে যোগাযোগ করতে চান, আমাদের একটি"}, 834 | {"info2", "বার্তা"}, 835 | {"info3", "পাঠান।"}, 836 | {"generalError", "কিছু ভুল হয়েছে। দয়া করে পরে আবার চেষ্টা করুন।"}, 837 | {"encryptNetworkError", "কিছু ভুল হয়েছে। আমি বার্তাটি সংরক্ষণ করতে পারছি না। দয়া করে আবার চেষ্টা করুন।"}, 838 | {"decryptNetworkError", "কিছু ভুল হয়েছে। আমি বার্তাটি পড়তে পারছি না। দয়া করে আবার চেষ্টা করুন।"}, 839 | {"password", "পাসওয়ার্ড"}, 840 | {"password", "পাসওয়ার্ড"}, 841 | {"enterPasswordPlaceholder", "পাসওয়ার্ড লিখুন"}, 842 | {"linkIsCorrupted", "লিঙ্কটি ক্ষতিগ্রস্ত"}, 843 | }, 844 | language.Japanese: { 845 | {"title", "プライベートで安全なメモ - 秘密を安全に送信"}, 846 | {"header", "プライベートな安全なメモ"}, 847 | {"description", "安全な暗号化メッセージ"}, 848 | {"enterTextMessage", "暗号化するメッセージを入力してください"}, 849 | {"secureButton", "メッセージを暗号化"}, 850 | {"copyLink", "リンクをコピーして友達に送信してください。メッセージは、読まれた後または4週間後にすぐに削除されます。"}, 851 | {"copyLinkButton", "リンクをコピー"}, 852 | {"newMessageButton", "新しいメッセージ"}, 853 | {"decodedMessage", "復号化されたメッセージ"}, 854 | {"messageRead", "メッセージが読まれた、期限切れ、またはリンクが正しくありません"}, 855 | {"readMessageButton", "メッセージを復号化"}, 856 | {"infoHeader", "説明"}, 857 | {"info", "このツールは、さまざまな暗号化方法を使用して最大限のセキュリティを確保します。" + 858 | "リンクがなければ、メッセージを復号化することはできません。セキュリティをさらに強化するためにパスワードを使用してください。" + 859 | "ツールのソースコードはオープンで、"}, 860 | {"info1", "で閲覧できます。私たちに連絡したい場合は、"}, 861 | {"info2", "メッセージ"}, 862 | {"info3", "を送ってください。"}, 863 | {"generalError", "何か問題が発生しました。後でもう一度お試しください。"}, 864 | {"encryptNetworkError", "何か問題が発生しました。メッセージを保存できません。もう一度お試しください。"}, 865 | {"decryptNetworkError", "何か問題が発生しました。メッセージを読むことができません。もう一度お試しください。"}, 866 | {"password", "パスワード"}, 867 | {"enterPasswordPlaceholder", "パスワードを入力してください"}, 868 | {"linkIsCorrupted", "リンクが破損しています"}, 869 | }, 870 | } 871 | -------------------------------------------------------------------------------- /i18n/i18n.go: -------------------------------------------------------------------------------- 1 | package i18n 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "obliviate/logs" 7 | "sync" 8 | 9 | "golang.org/x/text/language" 10 | "golang.org/x/text/message" 11 | ) 12 | 13 | type translation map[string]string 14 | 15 | type i18n struct { 16 | translations map[string]translation 17 | matcher language.Matcher 18 | sync.Mutex 19 | } 20 | 21 | func NewTranslation() *i18n { 22 | 23 | var languages []language.Tag 24 | 25 | languages = append(languages, language.English) 26 | 27 | for tag, oneLanguage := range translationsSet { 28 | if tag != language.English { 29 | languages = append(languages, tag) 30 | } 31 | for _, entry := range oneLanguage { 32 | err := message.SetString(tag, entry.key, entry.msg) 33 | if err != nil { 34 | slog.Error("pair population error", logs.Error, err) 35 | } 36 | } 37 | } 38 | tr := i18n{ 39 | matcher: language.NewMatcher(languages), 40 | translations: make(map[string]translation), 41 | } 42 | return &tr 43 | } 44 | 45 | func (t *i18n) GetTranslation(ctx context.Context, acceptLanguage string) translation { 46 | var acceptedTag language.Tag 47 | 48 | acceptTagList, _, err := language.ParseAcceptLanguage(acceptLanguage) 49 | if err != nil { 50 | acceptedTag = language.English 51 | } else { 52 | acceptedTag, _, _ = t.matcher.Match(acceptTagList...) 53 | } 54 | acceptedBaseLang := acceptedTag.String()[:2] 55 | 56 | t.Lock() 57 | defer t.Unlock() 58 | 59 | if tran, ok := t.translations[acceptedBaseLang]; ok { 60 | slog.InfoContext(ctx, "translation exists", logs.Language, acceptedBaseLang) 61 | return tran 62 | } 63 | 64 | tran := translation{} 65 | printer := message.NewPrinter(acceptedTag) 66 | 67 | for tag, oneLanguage := range translationsSet { 68 | if tag.String()[:2] == acceptedBaseLang { 69 | for _, entry := range oneLanguage { 70 | tran[entry.key] = printer.Sprintf(entry.key) 71 | } 72 | } 73 | } 74 | 75 | if len(tran) == 0 { 76 | slog.ErrorContext(ctx, "could not determine translation", logs.LanguageTag, acceptedTag, logs.Language, acceptedBaseLang) 77 | } 78 | 79 | t.translations[acceptedBaseLang] = tran 80 | 81 | slog.InfoContext(ctx, "language created", logs.Language, acceptedBaseLang) 82 | return tran 83 | } 84 | -------------------------------------------------------------------------------- /i18n/i18n_test.go: -------------------------------------------------------------------------------- 1 | package i18n 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | type testFields struct { 11 | tag, expected string 12 | } 13 | 14 | var testData = []testFields{ 15 | {"pl-PL,pl;q=0.9,en-US;q=0.8,en;q=0.7", "Prywatne wiadomości"}, 16 | {"fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5", "Notes privées sécurisées"}, 17 | {"en-ca,en;q=0.8,en-us;q=0.6,de-de;q=0.4,de;q=0.2", "Private secure notes"}, 18 | {"da, en-gb:q=0.5, en:q=0.4", "Private secure notes"}, 19 | } 20 | 21 | func TestI18n_GetLazyTranslation(t *testing.T) { 22 | 23 | trans := NewTranslation() 24 | for _, list := range testData { 25 | translation := trans.GetTranslation(context.Background(), list.tag) 26 | 27 | var msg string 28 | var ok bool 29 | if msg, ok = translation["header"]; !ok { 30 | assert.True(t, ok, "header not found") 31 | } 32 | assert.Equal(t, msg, list.expected, "translation error") 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /logs/main.go: -------------------------------------------------------------------------------- 1 | // Package logs level supported by Cloud Logging 2 | // https://github.com/remko/cloudrun-slog?tab=readme-ov-file 3 | package logs 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "log/slog" 9 | "net/http" 10 | "os" 11 | "strings" 12 | ) 13 | 14 | const ( 15 | Error = "error" 16 | Counter = "counter" 17 | JSON = "json" 18 | TemplateData = "template_data" 19 | Language = "language" 20 | LanguageTag = "language_tag" 21 | Length = "length" 22 | Country = "country" 23 | Time = "time" 24 | AcceptedLang = "accepted-language" 25 | NumDeleted = "number_deleted" 26 | Key = "key" 27 | ) 28 | 29 | // CloudLoggingHandler that outputs JSON understood by the structured log agent. 30 | // See https://cloud.google.com/logging/docs/agent/logging/configuration#special-fields 31 | type CloudLoggingHandler struct{ handler slog.Handler } 32 | 33 | // WithCloudTraceContext Middleware that adds the Cloud Trace ID to the context 34 | // This is used to correlate the structured logs with the Cloud Run 35 | // request log. 36 | func WithCloudTraceContext(h http.Handler) http.Handler { 37 | projectID := os.Getenv("OBLIVIATE_PROJECT_ID") 38 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 39 | var trace string 40 | traceHeader := r.Header.Get("X-Cloud-Trace-Context") 41 | traceParts := strings.Split(traceHeader, "/") 42 | if len(traceParts) > 0 && len(traceParts[0]) > 0 { 43 | trace = fmt.Sprintf("projects/%s/traces/%s", projectID, traceParts[0]) 44 | } 45 | //nolint:staticcheck 46 | h.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), "trace", trace))) 47 | }) 48 | } 49 | 50 | func traceFromContext(ctx context.Context) string { 51 | trace := ctx.Value("trace") 52 | if trace == nil { 53 | return "" 54 | } 55 | return trace.(string) 56 | } 57 | 58 | func NewCloudLoggingHandler(logLevel slog.Level) *CloudLoggingHandler { 59 | return &CloudLoggingHandler{handler: slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{ 60 | Level: logLevel, 61 | ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { 62 | if a.Key == slog.MessageKey { 63 | a.Key = "message" 64 | } else if a.Key == slog.LevelKey { 65 | a.Key = "severity" 66 | } 67 | return a 68 | }, 69 | })} 70 | } 71 | 72 | func (h *CloudLoggingHandler) Enabled(ctx context.Context, level slog.Level) bool { 73 | return h.handler.Enabled(ctx, level) 74 | } 75 | 76 | func (h *CloudLoggingHandler) Handle(ctx context.Context, rec slog.Record) error { 77 | trace := traceFromContext(ctx) 78 | if trace != "" { 79 | rec = rec.Clone() 80 | // Add trace ID to the record so it is correlated with the Cloud Run request log 81 | // See https://cloud.google.com/trace/docs/trace-log-integration 82 | rec.Add("logging.googleapis.com/trace", slog.StringValue(trace)) 83 | } 84 | return h.handler.Handle(ctx, rec) 85 | } 86 | 87 | func (h *CloudLoggingHandler) WithAttrs(attrs []slog.Attr) slog.Handler { 88 | return &CloudLoggingHandler{handler: h.handler.WithAttrs(attrs)} 89 | } 90 | 91 | func (h *CloudLoggingHandler) WithGroup(name string) slog.Handler { 92 | return &CloudLoggingHandler{handler: h.handler.WithGroup(name)} 93 | } 94 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "embed" 6 | "fmt" 7 | "log/slog" 8 | "net/http" 9 | "os" 10 | "time" 11 | 12 | _ "golang.org/x/crypto/x509roots/fallback" 13 | 14 | "obliviate/logs" 15 | 16 | "github.com/go-chi/chi/v5" 17 | "github.com/go-chi/chi/v5/middleware" 18 | "github.com/go-chi/cors" 19 | 20 | "obliviate/app" 21 | "obliviate/config" 22 | "obliviate/crypt" 23 | "obliviate/crypt/rsa" 24 | "obliviate/handler" 25 | "obliviate/repository" 26 | "obliviate/repository/mock" 27 | ) 28 | 29 | const ( 30 | messageDurationTime = time.Hour * 24 * 7 * 4 31 | ) 32 | 33 | //go:embed variables.json 34 | //go:embed web/build/* 35 | var static embed.FS 36 | 37 | func main() { 38 | conf := config.Configuration{ 39 | DefaultDurationTime: messageDurationTime, 40 | ProdEnv: os.Getenv("ENV") == "PROD", 41 | MasterKey: os.Getenv("HSM_MASTER_KEY"), 42 | KmsCredentialFile: os.Getenv("KMS_CREDENTIAL_FILE"), 43 | FirestoreCredentialFile: os.Getenv("FIRESTORE_CREDENTIAL_FILE"), 44 | StaticFilesLocation: "web/build", 45 | EmbededStaticFiles: static, 46 | } 47 | 48 | var algorithm rsa.EncryptionOnRest 49 | var db repository.DataBase 50 | 51 | if conf.ProdEnv { 52 | logger := slog.New(logs.NewCloudLoggingHandler(slog.LevelInfo)) 53 | dbPrefix := "" 54 | if os.Getenv("STAGE") != "prod" { 55 | dbPrefix = "test_" 56 | logger = slog.New(logs.NewCloudLoggingHandler(slog.LevelDebug)) 57 | } 58 | slog.SetDefault(logger) 59 | 60 | db = repository.NewConnection(context.Background(), conf.FirestoreCredentialFile, 61 | os.Getenv("OBLIVIATE_PROJECT_ID"), dbPrefix, conf.ProdEnv) 62 | algorithm = rsa.NewAlgorithm() 63 | } else { 64 | slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))) 65 | db = mock.StorageMock() 66 | algorithm = rsa.NewMockAlgorithm() 67 | slog.Info("Mock DB and encryption started") 68 | } 69 | 70 | keys, err := crypt.NewKeys(db, &conf, algorithm, true) 71 | if err != nil { 72 | slog.Error("error getting keys", logs.Error, err) 73 | } 74 | 75 | app := app.NewApp(db, &conf, keys) 76 | 77 | r := chi.NewRouter() 78 | 79 | if !conf.ProdEnv { 80 | r.Use(cors.Handler(cors.Options{ 81 | AllowedOrigins: []string{"https://localhost:5173", "http://localhost:5173"}, 82 | AllowedMethods: []string{"GET", "POST", "DELETE", "OPTIONS"}, 83 | AllowedHeaders: []string{"Content-Type"}, 84 | })) 85 | } 86 | 87 | compressor := middleware.NewCompressor(5, "text/html", "text/javascript", "application/javascript", "text/css", "image/x-icon", "text/plain", "application/json") 88 | r.Use(compressor.Handler) 89 | r.Use(logs.WithCloudTraceContext) 90 | 91 | r.Get("/*", handler.StaticFiles(&conf, true)) 92 | r.Get("/variables", handler.ProcessTemplate(&conf, keys.PublicKeyEncoded)) 93 | r.Post("/save", handler.Save(app)) 94 | r.Post("/read", handler.Read(app)) 95 | r.Delete("/expired", handler.Expired(app)) 96 | r.Delete("/delete", handler.Delete(app)) 97 | 98 | port := os.Getenv("PORT") 99 | if port == "" { 100 | port = "3000" 101 | } 102 | 103 | slog.Info("Service ready") 104 | err = http.ListenAndServe(fmt.Sprintf(":%s", port), r) 105 | if err != nil { 106 | slog.Error("Error ListenAndServe", logs.Error, err) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /repository/counter.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math/rand" 7 | "strconv" 8 | 9 | "cloud.google.com/go/firestore" 10 | "google.golang.org/api/iterator" 11 | ) 12 | 13 | // Counter is a collection of documents (shards) 14 | // to realize counter with high frequency. 15 | type Counter struct { 16 | numShards int 17 | collectionName string 18 | client *firestore.Client 19 | } 20 | 21 | // Shard is a single counter, which is used in a group 22 | // of other shards within Counter. 23 | type Shard struct { 24 | Count int 25 | } 26 | 27 | // initCounter creates a given number of shards as 28 | // subcollection of specified document. 29 | func (c *Counter) initCounter(ctx context.Context) error { 30 | colRef := c.client.Collection(c.collectionName) 31 | 32 | // Initialize each shard with count=0 33 | for num := 0; num < c.numShards; num++ { 34 | shard := Shard{0} 35 | 36 | if _, err := colRef.Doc(strconv.Itoa(num)).Set(ctx, shard); err != nil { 37 | return fmt.Errorf("Set: %v", err) 38 | } 39 | } 40 | return nil 41 | } 42 | 43 | // incrementCounter increments a randomly picked shard. 44 | func (c *Counter) incrementCounter(ctx context.Context) (*firestore.WriteResult, error) { 45 | docID := strconv.Itoa(rand.Intn(c.numShards)) 46 | 47 | shardRef := c.client.Collection(c.collectionName).Doc(docID) 48 | return shardRef.Update(ctx, []firestore.Update{ 49 | {Path: "Count", Value: firestore.Increment(1)}, 50 | }) 51 | } 52 | 53 | // getCount returns a total count across all shards. 54 | func (c *Counter) getCount(ctx context.Context) (int64, error) { 55 | var total int64 56 | shards := c.client.Collection(c.collectionName).Documents(ctx) 57 | for { 58 | doc, err := shards.Next() 59 | if err == iterator.Done { 60 | break 61 | } 62 | if err != nil { 63 | return 0, fmt.Errorf("Next: %v", err) 64 | } 65 | 66 | vTotal := doc.Data()["Count"] 67 | shardCount, ok := vTotal.(int64) 68 | if !ok { 69 | return 0, fmt.Errorf("firestore: invalid dataType %T, want int64", vTotal) 70 | } 71 | total += shardCount 72 | } 73 | return total, nil 74 | } 75 | 76 | func (c *Counter) counterExists(ctx context.Context) bool { 77 | _, err := c.client.Collection(c.collectionName).Doc("0").Get(ctx) 78 | return err == nil 79 | } 80 | -------------------------------------------------------------------------------- /repository/firestore.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log/slog" 7 | "time" 8 | 9 | "cloud.google.com/go/firestore" 10 | firebase "firebase.google.com/go" 11 | "google.golang.org/api/iterator" 12 | "google.golang.org/api/option" 13 | "google.golang.org/grpc/codes" 14 | "google.golang.org/grpc/status" 15 | 16 | "obliviate/config" 17 | "obliviate/logs" 18 | "obliviate/repository/model" 19 | ) 20 | 21 | const counterShards = 5 22 | 23 | type DataBase interface { 24 | SaveMessage(ctx context.Context, data model.MessageModel) error 25 | GetMessage(context.Context, string) (model.MessageType, error) 26 | DeleteMessage(context.Context, string) 27 | DeleteBeforeNow(context.Context) error 28 | SaveEncryptedKeys(context.Context, []byte) error 29 | GetEncryptedKeys(context.Context) ([]byte, error) 30 | IncreaseCounter(context.Context) 31 | } 32 | 33 | type collection struct { 34 | coll string 35 | keyColl string 36 | keyDoc string 37 | } 38 | 39 | type db struct { 40 | client *firestore.Client 41 | messageCollection collection 42 | counter Counter 43 | } 44 | 45 | func NewConnection(ctx context.Context, firestoreCredentialFile, projectID, prefix string, prodEnv bool) *db { 46 | var err error 47 | var app *firebase.App 48 | 49 | if prodEnv { 50 | conf := &firebase.Config{ProjectID: projectID} 51 | app, err = firebase.NewApp(ctx, conf) 52 | } else { 53 | sa := option.WithCredentialsFile(firestoreCredentialFile) 54 | app, err = firebase.NewApp(ctx, nil, sa) 55 | } 56 | 57 | if err != nil { 58 | slog.ErrorContext(ctx, "Cannot create new App while connecting to firestore", logs.Error, err) 59 | } 60 | client, err := app.Firestore(ctx) 61 | if err != nil { 62 | slog.ErrorContext(ctx, "Cannot create new client while connecting to firestore", logs.Error, err) 63 | } 64 | 65 | d := db{ 66 | messageCollection: collection{coll: prefix + "messages", keyColl: prefix + "commons", keyDoc: "keys"}, 67 | counter: Counter{counterShards, prefix + "stats", client}, 68 | client: client, 69 | } 70 | slog.InfoContext(ctx, "Firestore connected") 71 | 72 | if !d.counter.counterExists(ctx) { 73 | if err := d.counter.initCounter(ctx); err != nil { 74 | slog.ErrorContext(ctx, "Could not initialize the counters", logs.Error, err) 75 | panic("Could not initialize the counters") 76 | } 77 | slog.InfoContext(ctx, "Counter initialized") 78 | } 79 | 80 | i, _ := d.counter.getCount(ctx) 81 | slog.InfoContext(ctx, fmt.Sprintf("Counter = %d", i), logs.Counter, i) 82 | 83 | return &d 84 | } 85 | 86 | func (d *db) SaveMessage(ctx context.Context, data model.MessageModel) error { 87 | _, err := d.client.Collection(d.messageCollection.coll).Doc(data.Key()).Set(ctx, data.Message) 88 | if err != nil { 89 | return fmt.Errorf("error while saving key: %s, err: %v", data.Key(), err) 90 | } 91 | slog.InfoContext(ctx, 92 | fmt.Sprintf("message saved t: %d, len: %d, c: %s", data.Message.Time, len(data.Message.Txt), data.Message.Country), 93 | logs.Length, len(data.Message.Txt), logs.Country, data.Message.Country, logs.Time, data.Message.Time, logs.AcceptedLang, ctx.Value(config.AcceptLanguage)) 94 | return nil 95 | } 96 | 97 | func (d *db) GetMessage(ctx context.Context, key string) (model.MessageType, error) { 98 | 99 | data := model.MessageModel{} 100 | 101 | doc, err := d.client.Collection(d.messageCollection.coll).Doc(key).Get(ctx) 102 | if err != nil { 103 | if status.Code(err) != codes.NotFound { 104 | return data.Message, fmt.Errorf("error while getting message, err: %v", err) 105 | } 106 | slog.InfoContext(ctx, "message not found") 107 | return data.Message, nil 108 | } 109 | 110 | if err := doc.DataTo(&data.Message); err != nil { 111 | slog.InfoContext(ctx, "message found") 112 | return data.Message, fmt.Errorf("error mapping data into message struct: %v", err) 113 | } 114 | 115 | if data.Message.ValidTo.Before(time.Now()) { 116 | slog.WarnContext(ctx, "message found but not valid") 117 | return data.Message, nil 118 | } 119 | 120 | return data.Message, nil 121 | } 122 | 123 | func (d *db) DeleteMessage(ctx context.Context, key string) { 124 | _, err := d.client.Collection(d.messageCollection.coll).Doc(key).Delete(ctx) 125 | if err != nil { 126 | slog.ErrorContext(ctx, "cannot remove doc", logs.Key, key) 127 | } 128 | } 129 | 130 | func (d *db) DeleteBeforeNow(ctx context.Context) error { 131 | // https://firebase.google.com/docs/firestore/manage-data/delete-data#go 132 | numDeleted := 0 133 | iter := d.client.Collection(d.messageCollection.coll).Where("valid", "<", time.Now()).Documents(ctx) 134 | bulkWriter := d.client.BulkWriter(ctx) 135 | for { 136 | doc, err := iter.Next() 137 | if err == iterator.Done { 138 | break 139 | } 140 | if err != nil { 141 | return fmt.Errorf("failed to iterate: %v", err) 142 | } 143 | _, _ = bulkWriter.Delete(doc.Ref) 144 | numDeleted++ 145 | } 146 | if numDeleted == 0 { 147 | slog.WarnContext(ctx, "Nothing to delete") 148 | bulkWriter.End() 149 | return nil 150 | } 151 | bulkWriter.Flush() 152 | slog.InfoContext(ctx, fmt.Sprintf("Deleted %d documents", numDeleted), logs.NumDeleted, numDeleted) 153 | return nil 154 | } 155 | 156 | // ----------------------------------------------------------- 157 | 158 | func (d *db) SaveEncryptedKeys(ctx context.Context, encrypted []byte) error { 159 | keys := model.Key{Key: encrypted} 160 | _, err := d.client.Collection(d.messageCollection.keyColl).Doc(d.messageCollection.keyDoc).Set(ctx, keys) 161 | if err != nil { 162 | return fmt.Errorf("error while saving encrypted keys: %s, err: %v", keys.Key, err) 163 | } 164 | slog.InfoContext(ctx, "encrypted keys saved") 165 | return nil 166 | } 167 | 168 | func (d *db) GetEncryptedKeys(ctx context.Context) ([]byte, error) { 169 | doc, err := d.client.Collection(d.messageCollection.keyColl).Doc(d.messageCollection.keyDoc).Get(ctx) 170 | if err != nil { 171 | if status.Code(err) != codes.NotFound { 172 | return nil, fmt.Errorf("error while getting encrypted keys: %v", err) 173 | } 174 | return nil, nil 175 | } 176 | key := model.Key{} 177 | if err := doc.DataTo(&key); err != nil { 178 | return nil, fmt.Errorf("error mapping data into key struct: %v\n", err) 179 | } 180 | slog.DebugContext(ctx, "encrypted keys fetched from db") 181 | return key.Key, nil 182 | } 183 | 184 | func (d *db) IncreaseCounter(ctx context.Context) { 185 | _, err := d.counter.incrementCounter(ctx) 186 | if err != nil { 187 | slog.ErrorContext(ctx, "Increase counter error", logs.Error, err) 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /repository/mock/mock.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "context" 5 | "log/slog" 6 | "obliviate/logs" 7 | "time" 8 | 9 | model "obliviate/repository/model" 10 | ) 11 | 12 | type db struct { 13 | messageStore map[string]model.MessageModel 14 | encrypted []byte 15 | } 16 | 17 | func StorageMock() *db { 18 | d := db{} 19 | d.messageStore = make(map[string]model.MessageModel) 20 | return &d 21 | } 22 | 23 | func (d *db) SaveMessage(ctx context.Context, data model.MessageModel) error { 24 | d.messageStore[data.Key()] = data 25 | acceptLanguage := ctx.Value("Accept-Language") 26 | slog.Info("message saved", logs.Key, data.Key(), logs.AcceptedLang, acceptLanguage) 27 | return nil 28 | } 29 | 30 | func (d *db) GetMessage(ctx context.Context, key string) (model.MessageType, error) { 31 | if m, ok := d.messageStore[key]; ok { 32 | slog.Debug("key found", logs.Key, m.Key()) 33 | return m.Message, nil 34 | } else { 35 | slog.Debug("key not found", logs.Key, m.Key()) 36 | return m.Message, nil 37 | } 38 | } 39 | 40 | func (d *db) DeleteMessage(ctx context.Context, key string) { 41 | delete(d.messageStore, key) 42 | } 43 | 44 | func (d *db) DeleteBeforeNow(ctx context.Context) error { 45 | for _, v := range d.messageStore { 46 | if v.Message.ValidTo.Before(time.Now()) { 47 | d.DeleteMessage(ctx, v.Key()) 48 | } 49 | } 50 | return nil 51 | } 52 | 53 | func (d *db) SaveEncryptedKeys(ctx context.Context, encrypted []byte) error { 54 | d.encrypted = encrypted 55 | return nil 56 | } 57 | 58 | func (d *db) GetEncryptedKeys(ctx context.Context) ([]byte, error) { 59 | return d.encrypted, nil 60 | } 61 | 62 | func (d *db) IncreaseCounter(ctx context.Context) { 63 | } 64 | -------------------------------------------------------------------------------- /repository/model/key.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type Key struct { 4 | Key []byte `firestore:"key"` 5 | } 6 | -------------------------------------------------------------------------------- /repository/model/message.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type MessageType struct { 8 | Txt []byte `firestore:"txt"` 9 | ValidTo time.Time `firestore:"valid"` 10 | Nonce []byte `firestore:"nonce"` 11 | PublicKey []byte `firestore:"publicKey"` 12 | Time int `firestore:"time,omitempty"` 13 | CostFactor int `firestore:"costFactor,omitempty"` 14 | Country string `firestore:"country,omitempty"` 15 | } 16 | 17 | type MessageModel struct { 18 | key string 19 | Message MessageType 20 | } 21 | 22 | func NewMessage(key string, txt []byte, valid time.Time, nonce []byte, publicKey []byte, time int, costFactor int, country string) MessageModel { 23 | m := MessageModel{ 24 | key: key, 25 | Message: MessageType{ 26 | Txt: txt, 27 | ValidTo: valid, 28 | Nonce: nonce, 29 | PublicKey: publicKey, 30 | Time: time, 31 | CostFactor: costFactor, 32 | Country: country, 33 | }, 34 | } 35 | return m 36 | } 37 | 38 | func (m MessageModel) Key() string { 39 | return m.key 40 | } 41 | -------------------------------------------------------------------------------- /revive.toml: -------------------------------------------------------------------------------- 1 | ignoreGeneratedHeader = false 2 | severity = "warning" 3 | confidence = 0.8 4 | errorCode = 1 5 | warningCode = 2 6 | 7 | [rule.blank-imports] 8 | [rule.context-as-argument] 9 | [rule.context-keys-type] 10 | [rule.dot-imports] 11 | [rule.error-return] 12 | [rule.error-strings] 13 | [rule.error-naming] 14 | [rule.if-return] 15 | [rule.increment-decrement] 16 | [rule.var-naming] 17 | [rule.var-declaration] 18 | [rule.range] 19 | [rule.receiver-naming] 20 | [rule.time-naming] 21 | [rule.unexported-return] 22 | [rule.indent-error-flow] 23 | [rule.errorf] 24 | [rule.empty-block] 25 | [rule.superfluous-else] 26 | [rule.unreachable-code] 27 | [rule.redefines-builtin-id] -------------------------------------------------------------------------------- /variables.json: -------------------------------------------------------------------------------- 1 | { 2 | "PublicKey": "{{.PublicKey}}", 3 | "header": "{{.header}}", 4 | "enterTextMessage": "{{.enterTextMessage}}", 5 | "password": "{{.password}}", 6 | "secureButton": "{{.secureButton}}", 7 | "infoHeader": "{{.infoHeader}}", 8 | "info": "{{.info}}", 9 | "info1": "{{.info1}}", 10 | "info2": "{{.info2}}", 11 | "info3": "{{.info3}}", 12 | "encryptNetworkError": "{{.encryptNetworkError}}", 13 | "copyLink": "{{.copyLink}}", 14 | "copyLinkButton": "{{.copyLinkButton}}", 15 | "newMessageButton": "{{.newMessageButton}}", 16 | "messageRead": "{{.messageRead}}", 17 | "enterPasswordPlaceholder": "{{.enterPasswordPlaceholder}}", 18 | "readMessageButton": "{{.readMessageButton}}", 19 | "linkIsCorrupted": "{{.linkIsCorrupted}}", 20 | "generalError": "{{.generalError}}", 21 | "decryptNetworkError": "{{.decryptNetworkError}}", 22 | "decodedMessage": "{{.decodedMessage}}", 23 | "title": "{{.title}}", 24 | "description": "{{.description}}" 25 | } -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | # Output 4 | .output 5 | .vercel 6 | /.svelte-kit 7 | /build 8 | 9 | # OS 10 | .DS_Store 11 | Thumbs.db 12 | 13 | # Env 14 | .env 15 | .env.* 16 | !.env.example 17 | !.env.test 18 | 19 | # Vite 20 | vite.config.js.timestamp-* 21 | vite.config.ts.timestamp-* 22 | -------------------------------------------------------------------------------- /web/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | # sv 2 | 3 | Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). 4 | 5 | ## Creating a project 6 | 7 | If you're seeing this, you've probably already done this step. Congrats! 8 | 9 | ```bash 10 | # create a new project in the current directory 11 | npx sv create 12 | 13 | # create a new project in my-app 14 | npx sv create my-app 15 | ``` 16 | 17 | ## Developing 18 | 19 | Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: 20 | 21 | ```bash 22 | npm run dev 23 | 24 | # or start the server and open the app in a new browser tab 25 | npm run dev -- --open 26 | ``` 27 | 28 | ## Building 29 | 30 | To create a production version of your app: 31 | 32 | ```bash 33 | npm run build 34 | ``` 35 | 36 | You can preview the production build with `npm run preview`. 37 | 38 | > To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. 39 | -------------------------------------------------------------------------------- /web/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler" 13 | } 14 | // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias 15 | // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files 16 | // 17 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 18 | // from the referenced tsconfig.json - TypeScript does not merge them in 19 | } 20 | -------------------------------------------------------------------------------- /web/migrate.txt: -------------------------------------------------------------------------------- 1 | npx sv create . 2 | npm install @sveltejs/adapter-static 3 | npm install @stablelib/base64 4 | npm install @stablelib/utf8 5 | npm install bootstrap 6 | npm install clipboard 7 | npm install scrypt-async 8 | npm install tweetnacl 9 | npx svelte-add@latest scss 10 | npm install 11 | #upgrade: ncu --upgrade --target minor --interactive 12 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "version": "0.0.1", 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite dev", 7 | "build": "vite build", 8 | "preview": "vite preview", 9 | "check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json", 10 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch" 11 | }, 12 | "devDependencies": { 13 | "@sveltejs/adapter-auto": "^3.3.1", 14 | "@sveltejs/kit": "^2.20.5", 15 | "@sveltejs/vite-plugin-svelte": "^4.0.4", 16 | "sass-embedded": "^1.86.3", 17 | "svelte": "^5.25.12", 18 | "svelte-check": "^4.1.5", 19 | "typescript": "^5.8.3", 20 | "vite": "^5.4.18" 21 | }, 22 | "dependencies": { 23 | "@stablelib/base64": "^2.0.1", 24 | "@stablelib/utf8": "^2.0.1", 25 | "@sveltejs/adapter-static": "^3.0.8", 26 | "bootstrap": "^5.3.5", 27 | "clipboard": "^2.0.11", 28 | "scrypt-async": "^2.0.1", 29 | "tweetnacl": "^1.0.3" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /web/src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface PageState {} 9 | // interface Platform {} 10 | } 11 | } 12 | 13 | export {}; 14 | -------------------------------------------------------------------------------- /web/src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Private and secure notes, send your secrets safely 6 | 7 | 8 | 9 | 10 | 11 | 12 | %sveltekit.head% 13 | 91 | 97 | 98 | 99 |
loading....
100 |
%sveltekit.body%
101 | 102 | 103 | -------------------------------------------------------------------------------- /web/src/app.scss: -------------------------------------------------------------------------------- 1 | /* Write your global styles here, in SCSS syntax. Variables and mixins from the src/variables.scss file are available here without importing */ 2 | -------------------------------------------------------------------------------- /web/src/lib/index.js: -------------------------------------------------------------------------------- 1 | // place files you want to import through the `$lib` alias in this folder. 2 | -------------------------------------------------------------------------------- /web/src/routes/+layout.js: -------------------------------------------------------------------------------- 1 | export const prerender = true 2 | export const ssr = false -------------------------------------------------------------------------------- /web/src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /web/src/routes/+page.js: -------------------------------------------------------------------------------- 1 | import {CONSTANTS} from './Commons.js' 2 | import * as base64 from '@stablelib/base64' 3 | 4 | export const prerender = true 5 | 6 | let vars = { 7 | serverPublicKey: new Uint8Array(), 8 | copyLink: "", 9 | copyLinkButton: "", 10 | decodedMessage: "", 11 | decryptNetworkError: "", 12 | description: "", 13 | encryptNetworkError: "", 14 | enterTextMessage: "", 15 | generalError: "", 16 | header: "", 17 | info: "", 18 | info1: "", 19 | info2: "", 20 | info3: "", 21 | infoHeader: "", 22 | linkIsCorrupted: "", 23 | messageRead: "", 24 | newMessageButton: "", 25 | password: "", 26 | enterPasswordPlaceholder: "", 27 | readMessageButton: "", 28 | secureButton: "", 29 | title: "" 30 | } 31 | 32 | /** @type {import('./$types').PageLoad} */ 33 | export async function load({fetch}) { 34 | try { 35 | const res = await fetch(CONSTANTS.VARIABLES_URL) 36 | if (!res.ok) { 37 | throw new Error("Server error, status: " + res.status) 38 | } 39 | 40 | const data = await res.json() 41 | vars = data 42 | vars.serverPublicKey = base64.decode(data.PublicKey) 43 | return vars 44 | } catch (error) { 45 | console.error("Network error: ", error) 46 | alert("Something went wrong. Try again.") 47 | } 48 | } -------------------------------------------------------------------------------- /web/src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 45 | 46 |

{data.header}

47 |
48 |
49 | {#if visible === parts.ENCRYPT} 50 | 51 | {:else if visible === parts.LINK} 52 | 53 | {:else if visible === parts.DECRYPT} 54 | 55 | {:else if visible === parts.SHOW} 56 | 57 | {/if} 58 |
59 | 60 |
61 |
62 |
63 |
64 |
65 |
66 |
{data.infoHeader}
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |

76 | {data.info} 77 | GitHub{data.info1} 79 | {data.info2}{data.info3} 81 | 82 |

83 |
84 |
85 |
86 |
87 |
88 | -------------------------------------------------------------------------------- /web/src/routes/Commons.js: -------------------------------------------------------------------------------- 1 | import scryptAsynch from "scrypt-async" 2 | import nacl from "tweetnacl" 3 | 4 | const MODE = import.meta.env.MODE 5 | 6 | export const CONSTANTS = { 7 | VARIABLES_URL: MODE === 'development' ? 'http://localhost:3000/variables' : '/variables', 8 | SAVE_URL: MODE === 'development' ? 'http://localhost:3000/save' : '/save', 9 | READ_URL: MODE === 'development' ? 'http://localhost:3000/read' : '/read', 10 | DELETE_URL: MODE === 'development' ? 'http://localhost:3000/delete' : '/delete', 11 | costFactor: 15, 12 | queryIndexWithPassword: 4 13 | } 14 | 15 | export const calculateKeyDerived = function (password, salt, logN, callback) { 16 | try { 17 | const t1 = getTime() 18 | scryptAsynch(password, salt, { 19 | logN: logN, 20 | r: 8, 21 | p: 1, 22 | dkLen: nacl.secretbox.keyLength, // 32 23 | interruptStep: 0, 24 | encoding: 'binary' // hex, base64, binary 25 | }, 26 | function (res) { 27 | const time = Math.round(getTime() - t1) 28 | callback(res, time) 29 | } 30 | ) 31 | } catch (ex) { 32 | alert(ex.message) 33 | } 34 | } 35 | 36 | var getTime = (function () { 37 | if (typeof performance !== "undefined") { 38 | return performance.now.bind(performance) 39 | } 40 | return Date.now.bind(Date) 41 | })() 42 | 43 | export const post = function(method, webObject, url, postSuccess, postError) { 44 | fetch(url, { 45 | method: method, 46 | headers: { 47 | 'Content-Type': 'application/json', 48 | }, 49 | body: JSON.stringify(webObject), 50 | }) 51 | .then(response => { 52 | if (!response.ok) { 53 | throw new Error(`HTTP error! Status: ${response.status}`) 54 | } 55 | return response.json() 56 | }) 57 | .then(data => { 58 | postSuccess(data) 59 | }) 60 | .catch(err => { 61 | postError(err) 62 | }) 63 | } -------------------------------------------------------------------------------- /web/src/routes/Decrypt.svelte: -------------------------------------------------------------------------------- 1 | 207 | 208 |
209 | {#if messageReadInfo} 210 |
211 |
212 |

{data.messageRead}

213 |
214 |
215 | {:else} 216 | {#if hasPassword} 217 |
218 |
219 |
220 |
221 | {data.password} 222 |
223 | 229 |
230 |
231 |
232 | {/if} 233 |
234 |
235 | 240 |
241 |
242 | 245 |
246 |
247 | {/if} 248 |
-------------------------------------------------------------------------------- /web/src/routes/Encrypt.svelte: -------------------------------------------------------------------------------- 1 | 152 | 153 |

{data.enterTextMessage}

154 |
155 |
156 |