diff --git a/.env.template b/.env.template index 8e5fab73..34e0e294 100644 --- a/.env.template +++ b/.env.template @@ -28,6 +28,7 @@ # RSA_KEY_FILENAME=data/rsa_key # ICON_CACHE_FOLDER=data/icon_cache # ATTACHMENTS_FOLDER=data/attachments +# SENDS_FOLDER=data/sends ## Templates data folder, by default uses embedded templates ## Check source code to see the format diff --git a/Cargo.lock b/Cargo.lock index 8c574491..419d1700 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -224,9 +224,9 @@ checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" [[package]] name = "byteorder" -version = "1.4.2" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae44d1a3d5a19df61dd0c8beb138458ac2a53a7ac09eba97d55592540004306b" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" @@ -281,6 +281,7 @@ dependencies = [ "libc", "num-integer", "num-traits", + "serde", "time 0.1.44", "winapi 0.3.9", ] @@ -400,7 +401,7 @@ dependencies = [ "bitflags", "proc-macro2 1.0.24", "quote 1.0.9", - "syn 1.0.62", + "syn 1.0.63", ] [[package]] @@ -428,7 +429,7 @@ checksum = "45f5098f628d02a7a0f68ddba586fb61e80edec3bdc1be3b921f4ceec60858d3" dependencies = [ "proc-macro2 1.0.24", "quote 1.0.9", - "syn 1.0.62", + "syn 1.0.63", ] [[package]] @@ -650,7 +651,7 @@ dependencies = [ "proc-macro-hack", "proc-macro2 1.0.24", "quote 1.0.9", - "syn 1.0.62", + "syn 1.0.63", ] [[package]] @@ -840,7 +841,7 @@ dependencies = [ "markup5ever", "proc-macro2 1.0.24", "quote 1.0.9", - "syn 1.0.62", + "syn 1.0.63", ] [[package]] @@ -1076,9 +1077,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "lettre" -version = "0.10.0-beta.1" +version = "0.10.0-beta.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f060515ec950723c5482163457b3faad8c088810c4a01a42523bc98e15bcb3e" +checksum = "a8dcca2a7f2c772f90dd58c3293ff4ac416486a7f0f4339a0d6775363e6bd12a" dependencies = [ "base64 0.13.0", "hostname", @@ -1223,7 +1224,7 @@ dependencies = [ "migrations_internals", "proc-macro2 1.0.24", "quote 1.0.9", - "syn 1.0.62", + "syn 1.0.63", ] [[package]] @@ -1442,7 +1443,7 @@ checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" dependencies = [ "proc-macro2 1.0.24", "quote 1.0.9", - "syn 1.0.62", + "syn 1.0.63", ] [[package]] @@ -1717,7 +1718,7 @@ dependencies = [ "pest_meta", "proc-macro2 1.0.24", "quote 1.0.9", - "syn 1.0.62", + "syn 1.0.63", ] [[package]] @@ -1792,7 +1793,7 @@ checksum = "758669ae3558c6f74bd2a18b41f7ac0b5a195aea6639d6a9b5e5d1ad5ba24c0b" dependencies = [ "proc-macro2 1.0.24", "quote 1.0.9", - "syn 1.0.62", + "syn 1.0.63", ] [[package]] @@ -2056,21 +2057,20 @@ dependencies = [ [[package]] name = "regex" -version = "1.4.3" +version = "1.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9251239e129e16308e70d853559389de218ac275b515068abc96829d05b948a" +checksum = "54fd1046a3107eb58f42de31d656fee6853e5d276c455fd943742dce89fc3dd3" dependencies = [ "aho-corasick", "memchr", "regex-syntax", - "thread_local", ] [[package]] name = "regex-syntax" -version = "0.6.22" +version = "0.6.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5eb417147ba9860a96cfe72a0b93bf88fee1744b5636ec99ab20c1aa9376581" +checksum = "24d5f089152e60f62d28b835fbff2cd2e8dc0baf1ac13343bef92ab7eed84548" [[package]] name = "remove_dir_all" @@ -2083,9 +2083,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0460542b551950620a3648c6aa23318ac6b3cd779114bd873209e6e8b5eb1c34" +checksum = "bf12057f289428dbf5c591c74bf10392e4a8003f993405a902f20117019022d4" dependencies = [ "base64 0.13.0", "bytes 1.0.1", @@ -2306,9 +2306,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dfd318104249865096c8da1dfabf09ddbb6d0330ea176812a62ec75e40c4166" +checksum = "d493c5f39e02dfb062cd8f33301f90f9b13b650e8c1b1d0fd75c19dd64bff69d" dependencies = [ "bitflags", "core-foundation", @@ -2359,7 +2359,7 @@ checksum = "1800f7693e94e186f5e25a28291ae1570da908aff7d97a095dec1e56ff99069b" dependencies = [ "proc-macro2 1.0.24", "quote 1.0.9", - "syn 1.0.62", + "syn 1.0.63", ] [[package]] @@ -2542,7 +2542,7 @@ dependencies = [ "quote 1.0.9", "serde", "serde_derive", - "syn 1.0.62", + "syn 1.0.63", ] [[package]] @@ -2558,7 +2558,7 @@ dependencies = [ "serde_derive", "serde_json", "sha1", - "syn 1.0.62", + "syn 1.0.63", ] [[package]] @@ -2611,9 +2611,9 @@ dependencies = [ [[package]] name = "syn" -version = "1.0.62" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "123a78a3596b24fee53a6464ce52d8ecbf62241e6294c7e7fe12086cd161f512" +checksum = "8fd9bc7ccc2688b3344c2f48b9b546648b25ce0b20fc717ee7fa7981a8ca9717" dependencies = [ "proc-macro2 1.0.24", "quote 1.0.9", @@ -2663,15 +2663,6 @@ dependencies = [ "utf-8", ] -[[package]] -name = "thread_local" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd" -dependencies = [ - "once_cell", -] - [[package]] name = "threadpool" version = "1.8.1" @@ -2727,7 +2718,7 @@ dependencies = [ "proc-macro2 1.0.24", "quote 1.0.9", "standback", - "syn 1.0.62", + "syn 1.0.63", ] [[package]] @@ -2747,9 +2738,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8190d04c665ea9e6b6a0dc45523ade572c088d2e6566244c1122671dbf4ae3a" +checksum = "8d56477f6ed99e10225f38f9f75f872f29b8b8bd8c0b946f63345bb144e9eeda" dependencies = [ "autocfg", "bytes 1.0.1", @@ -2772,9 +2763,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebb7cb2f00c5ae8df755b252306272cd1790d39728363936e01827e11f0b017b" +checksum = "ec31e5cc6b46e653cf57762f36f71d5e6386391d88a72fd6db4508f8f676fb29" dependencies = [ "bytes 1.0.1", "futures-core", @@ -2848,9 +2839,9 @@ checksum = "1410f6f91f21d1612654e7cc69193b0334f909dcf2c790c4826254fbb86f8887" [[package]] name = "typenum" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" +checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" [[package]] name = "u2f" @@ -3041,7 +3032,7 @@ dependencies = [ "log 0.4.14", "proc-macro2 1.0.24", "quote 1.0.9", - "syn 1.0.62", + "syn 1.0.63", "wasm-bindgen-shared", ] @@ -3075,7 +3066,7 @@ checksum = "cc053ec74d454df287b9374ee8abb36ffd5acb95ba87da3ba5b7d3fe20eb401e" dependencies = [ "proc-macro2 1.0.24", "quote 1.0.9", - "syn 1.0.62", + "syn 1.0.63", "wasm-bindgen-backend", "wasm-bindgen-shared", ] diff --git a/Cargo.toml b/Cargo.toml index 667bdb02..938be6e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ rocket = { version = "0.5.0-dev", features = ["tls"], default-features = false } rocket_contrib = "0.5.0-dev" # HTTP client -reqwest = { version = "0.11.1", features = ["blocking", "json"] } +reqwest = { version = "0.11.2", features = ["blocking", "json"] } # multipart/form-data support multipart = { version = "0.17.1", features = ["server"], default-features = false } @@ -69,7 +69,7 @@ ring = "0.16.20" uuid = { version = "0.8.2", features = ["v4"] } # Date and time libraries -chrono = "0.4.19" +chrono = { version = "0.4.19", features = ["serde"] } chrono-tz = "0.5.3" time = "0.2.25" @@ -99,7 +99,7 @@ num-traits = "0.2.14" num-derive = "0.3.3" # Email libraries -lettre = { version = "0.10.0-beta.1", features = ["smtp-transport", "builder", "serde", "native-tls", "hostname", "tracing"], default-features = false } +lettre = { version = "0.10.0-beta.2", features = ["smtp-transport", "builder", "serde", "native-tls", "hostname", "tracing"], default-features = false } newline-converter = "0.2.0" # Template library @@ -108,7 +108,7 @@ handlebars = { version = "3.5.3", features = ["dir_source"] } # For favicon extraction from main website html5ever = "0.25.1" markup5ever_rcdom = "0.1.0" -regex = { version = "1.4.3", features = ["std", "perf"], default-features = false } +regex = { version = "1.4.4", features = ["std", "perf"], default-features = false } data-url = "0.1.0" # Used by U2F, JWT and Postgres diff --git a/docker/Dockerfile.j2 b/docker/Dockerfile.j2 index 37f65c51..ec8e2301 100644 --- a/docker/Dockerfile.j2 +++ b/docker/Dockerfile.j2 @@ -44,8 +44,8 @@ # https://docs.docker.com/develop/develop-images/multistage-build/ # https://whitfin.io/speeding-up-rust-docker-builds/ ####################### VAULT BUILD IMAGE ####################### -{% set vault_version = "2.18.2" %} -{% set vault_image_digest = "sha256:c287301264c7dc86d89aa80487fd7960cc24048390d2bef9ef910dfc77d2c7d5" %} +{% set vault_version = "2.19.0" %} +{% set vault_image_digest = "sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4" %} # The web-vault digest specifies a particular web-vault build on Docker Hub. # Using the digest instead of the tag name provides better security, # as the digest of an image is immutable, whereas a tag name can later diff --git a/docker/amd64/Dockerfile b/docker/amd64/Dockerfile index b7f7a7d8..08cdb1a8 100644 --- a/docker/amd64/Dockerfile +++ b/docker/amd64/Dockerfile @@ -14,15 +14,15 @@ # - From https://hub.docker.com/r/bitwardenrs/web-vault/tags, # click the tag name to view the digest of the image it currently points to. # - From the command line: -# $ docker pull bitwardenrs/web-vault:v2.18.2 -# $ docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.18.2 -# [bitwardenrs/web-vault@sha256:c287301264c7dc86d89aa80487fd7960cc24048390d2bef9ef910dfc77d2c7d5] +# $ docker pull bitwardenrs/web-vault:v2.19.0 +# $ docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.19.0 +# [bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4] # # - Conversely, to get the tag name from the digest: -# $ docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:c287301264c7dc86d89aa80487fd7960cc24048390d2bef9ef910dfc77d2c7d5 -# [bitwardenrs/web-vault:v2.18.2] +# $ docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4 +# [bitwardenrs/web-vault:v2.19.0] # -FROM bitwardenrs/web-vault@sha256:c287301264c7dc86d89aa80487fd7960cc24048390d2bef9ef910dfc77d2c7d5 as vault +FROM bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4 as vault ########################## BUILD IMAGE ########################## FROM rust:1.50 as build diff --git a/docker/amd64/Dockerfile.alpine b/docker/amd64/Dockerfile.alpine index f242bd56..71d63132 100644 --- a/docker/amd64/Dockerfile.alpine +++ b/docker/amd64/Dockerfile.alpine @@ -14,15 +14,15 @@ # - From https://hub.docker.com/r/bitwardenrs/web-vault/tags, # click the tag name to view the digest of the image it currently points to. # - From the command line: -# $ docker pull bitwardenrs/web-vault:v2.18.2 -# $ docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.18.2 -# [bitwardenrs/web-vault@sha256:c287301264c7dc86d89aa80487fd7960cc24048390d2bef9ef910dfc77d2c7d5] +# $ docker pull bitwardenrs/web-vault:v2.19.0 +# $ docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.19.0 +# [bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4] # # - Conversely, to get the tag name from the digest: -# $ docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:c287301264c7dc86d89aa80487fd7960cc24048390d2bef9ef910dfc77d2c7d5 -# [bitwardenrs/web-vault:v2.18.2] +# $ docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4 +# [bitwardenrs/web-vault:v2.19.0] # -FROM bitwardenrs/web-vault@sha256:c287301264c7dc86d89aa80487fd7960cc24048390d2bef9ef910dfc77d2c7d5 as vault +FROM bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4 as vault ########################## BUILD IMAGE ########################## FROM clux/muslrust:nightly-2021-02-22 as build diff --git a/docker/arm64/Dockerfile b/docker/arm64/Dockerfile index 43cfbfcf..c6faf743 100644 --- a/docker/arm64/Dockerfile +++ b/docker/arm64/Dockerfile @@ -14,15 +14,15 @@ # - From https://hub.docker.com/r/bitwardenrs/web-vault/tags, # click the tag name to view the digest of the image it currently points to. # - From the command line: -# $ docker pull bitwardenrs/web-vault:v2.18.2 -# $ docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.18.2 -# [bitwardenrs/web-vault@sha256:c287301264c7dc86d89aa80487fd7960cc24048390d2bef9ef910dfc77d2c7d5] +# $ docker pull bitwardenrs/web-vault:v2.19.0 +# $ docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.19.0 +# [bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4] # # - Conversely, to get the tag name from the digest: -# $ docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:c287301264c7dc86d89aa80487fd7960cc24048390d2bef9ef910dfc77d2c7d5 -# [bitwardenrs/web-vault:v2.18.2] +# $ docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4 +# [bitwardenrs/web-vault:v2.19.0] # -FROM bitwardenrs/web-vault@sha256:c287301264c7dc86d89aa80487fd7960cc24048390d2bef9ef910dfc77d2c7d5 as vault +FROM bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4 as vault ########################## BUILD IMAGE ########################## FROM rust:1.50 as build diff --git a/docker/armv6/Dockerfile b/docker/armv6/Dockerfile index 4768bff3..c095e8c9 100644 --- a/docker/armv6/Dockerfile +++ b/docker/armv6/Dockerfile @@ -14,15 +14,15 @@ # - From https://hub.docker.com/r/bitwardenrs/web-vault/tags, # click the tag name to view the digest of the image it currently points to. # - From the command line: -# $ docker pull bitwardenrs/web-vault:v2.18.2 -# $ docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.18.2 -# [bitwardenrs/web-vault@sha256:c287301264c7dc86d89aa80487fd7960cc24048390d2bef9ef910dfc77d2c7d5] +# $ docker pull bitwardenrs/web-vault:v2.19.0 +# $ docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.19.0 +# [bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4] # # - Conversely, to get the tag name from the digest: -# $ docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:c287301264c7dc86d89aa80487fd7960cc24048390d2bef9ef910dfc77d2c7d5 -# [bitwardenrs/web-vault:v2.18.2] +# $ docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4 +# [bitwardenrs/web-vault:v2.19.0] # -FROM bitwardenrs/web-vault@sha256:c287301264c7dc86d89aa80487fd7960cc24048390d2bef9ef910dfc77d2c7d5 as vault +FROM bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4 as vault ########################## BUILD IMAGE ########################## FROM rust:1.50 as build diff --git a/docker/armv7/Dockerfile b/docker/armv7/Dockerfile index 60fc1fbf..a880d061 100644 --- a/docker/armv7/Dockerfile +++ b/docker/armv7/Dockerfile @@ -14,15 +14,15 @@ # - From https://hub.docker.com/r/bitwardenrs/web-vault/tags, # click the tag name to view the digest of the image it currently points to. # - From the command line: -# $ docker pull bitwardenrs/web-vault:v2.18.2 -# $ docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.18.2 -# [bitwardenrs/web-vault@sha256:c287301264c7dc86d89aa80487fd7960cc24048390d2bef9ef910dfc77d2c7d5] +# $ docker pull bitwardenrs/web-vault:v2.19.0 +# $ docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.19.0 +# [bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4] # # - Conversely, to get the tag name from the digest: -# $ docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:c287301264c7dc86d89aa80487fd7960cc24048390d2bef9ef910dfc77d2c7d5 -# [bitwardenrs/web-vault:v2.18.2] +# $ docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4 +# [bitwardenrs/web-vault:v2.19.0] # -FROM bitwardenrs/web-vault@sha256:c287301264c7dc86d89aa80487fd7960cc24048390d2bef9ef910dfc77d2c7d5 as vault +FROM bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4 as vault ########################## BUILD IMAGE ########################## FROM rust:1.50 as build diff --git a/docker/armv7/Dockerfile.alpine b/docker/armv7/Dockerfile.alpine index 768d5db0..8e656354 100644 --- a/docker/armv7/Dockerfile.alpine +++ b/docker/armv7/Dockerfile.alpine @@ -14,15 +14,15 @@ # - From https://hub.docker.com/r/bitwardenrs/web-vault/tags, # click the tag name to view the digest of the image it currently points to. # - From the command line: -# $ docker pull bitwardenrs/web-vault:v2.18.2 -# $ docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.18.2 -# [bitwardenrs/web-vault@sha256:c287301264c7dc86d89aa80487fd7960cc24048390d2bef9ef910dfc77d2c7d5] +# $ docker pull bitwardenrs/web-vault:v2.19.0 +# $ docker image inspect --format "{{.RepoDigests}}" bitwardenrs/web-vault:v2.19.0 +# [bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4] # # - Conversely, to get the tag name from the digest: -# $ docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:c287301264c7dc86d89aa80487fd7960cc24048390d2bef9ef910dfc77d2c7d5 -# [bitwardenrs/web-vault:v2.18.2] +# $ docker image inspect --format "{{.RepoTags}}" bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4 +# [bitwardenrs/web-vault:v2.19.0] # -FROM bitwardenrs/web-vault@sha256:c287301264c7dc86d89aa80487fd7960cc24048390d2bef9ef910dfc77d2c7d5 as vault +FROM bitwardenrs/web-vault@sha256:8747cfaa2c6d87d1749e119dd884697e8099389aa9aca30a4d73d4ff796fe0e4 as vault ########################## BUILD IMAGE ########################## FROM messense/rust-musl-cross:armv7-musleabihf as build diff --git a/migrations/mysql/2021-03-11-190243_add_sends/down.sql b/migrations/mysql/2021-03-11-190243_add_sends/down.sql new file mode 100644 index 00000000..b843b76a --- /dev/null +++ b/migrations/mysql/2021-03-11-190243_add_sends/down.sql @@ -0,0 +1 @@ +DROP TABLE sends; diff --git a/migrations/mysql/2021-03-11-190243_add_sends/up.sql b/migrations/mysql/2021-03-11-190243_add_sends/up.sql new file mode 100644 index 00000000..65b818a4 --- /dev/null +++ b/migrations/mysql/2021-03-11-190243_add_sends/up.sql @@ -0,0 +1,25 @@ +CREATE TABLE sends ( + uuid CHAR(36) NOT NULL PRIMARY KEY, + user_uuid CHAR(36) REFERENCES users (uuid), + organization_uuid CHAR(36) REFERENCES organizations (uuid), + + name TEXT NOT NULL, + notes TEXT, + + atype INTEGER NOT NULL, + data TEXT NOT NULL, + akey TEXT NOT NULL, + password_hash BLOB, + password_salt BLOB, + password_iter INTEGER, + + max_access_count INTEGER, + access_count INTEGER NOT NULL, + + creation_date DATETIME NOT NULL, + revision_date DATETIME NOT NULL, + expiration_date DATETIME, + deletion_date DATETIME NOT NULL, + + disabled BOOLEAN NOT NULL +); \ No newline at end of file diff --git a/migrations/postgresql/2021-03-11-190243_add_sends/down.sql b/migrations/postgresql/2021-03-11-190243_add_sends/down.sql new file mode 100644 index 00000000..b843b76a --- /dev/null +++ b/migrations/postgresql/2021-03-11-190243_add_sends/down.sql @@ -0,0 +1 @@ +DROP TABLE sends; diff --git a/migrations/postgresql/2021-03-11-190243_add_sends/up.sql b/migrations/postgresql/2021-03-11-190243_add_sends/up.sql new file mode 100644 index 00000000..c57ca93b --- /dev/null +++ b/migrations/postgresql/2021-03-11-190243_add_sends/up.sql @@ -0,0 +1,25 @@ +CREATE TABLE sends ( + uuid CHAR(36) NOT NULL PRIMARY KEY, + user_uuid CHAR(36) REFERENCES users (uuid), + organization_uuid CHAR(36) REFERENCES organizations (uuid), + + name TEXT NOT NULL, + notes TEXT, + + atype INTEGER NOT NULL, + data TEXT NOT NULL, + key TEXT NOT NULL, + password_hash BYTEA, + password_salt BYTEA, + password_iter INTEGER, + + max_access_count INTEGER, + access_count INTEGER NOT NULL, + + creation_date TIMESTAMP NOT NULL, + revision_date TIMESTAMP NOT NULL, + expiration_date TIMESTAMP, + deletion_date TIMESTAMP NOT NULL, + + disabled BOOLEAN NOT NULL +); \ No newline at end of file diff --git a/migrations/postgresql/2021-03-15-163412_rename_send_key/down.sql b/migrations/postgresql/2021-03-15-163412_rename_send_key/down.sql new file mode 100644 index 00000000..e69de29b diff --git a/migrations/postgresql/2021-03-15-163412_rename_send_key/up.sql b/migrations/postgresql/2021-03-15-163412_rename_send_key/up.sql new file mode 100644 index 00000000..08339fac --- /dev/null +++ b/migrations/postgresql/2021-03-15-163412_rename_send_key/up.sql @@ -0,0 +1 @@ +ALTER TABLE sends RENAME COLUMN key TO akey; diff --git a/migrations/sqlite/2021-03-11-190243_add_sends/down.sql b/migrations/sqlite/2021-03-11-190243_add_sends/down.sql new file mode 100644 index 00000000..b843b76a --- /dev/null +++ b/migrations/sqlite/2021-03-11-190243_add_sends/down.sql @@ -0,0 +1 @@ +DROP TABLE sends; diff --git a/migrations/sqlite/2021-03-11-190243_add_sends/up.sql b/migrations/sqlite/2021-03-11-190243_add_sends/up.sql new file mode 100644 index 00000000..0c1e17ba --- /dev/null +++ b/migrations/sqlite/2021-03-11-190243_add_sends/up.sql @@ -0,0 +1,25 @@ +CREATE TABLE sends ( + uuid TEXT NOT NULL PRIMARY KEY, + user_uuid TEXT REFERENCES users (uuid), + organization_uuid TEXT REFERENCES organizations (uuid), + + name TEXT NOT NULL, + notes TEXT, + + atype INTEGER NOT NULL, + data TEXT NOT NULL, + key TEXT NOT NULL, + password_hash BLOB, + password_salt BLOB, + password_iter INTEGER, + + max_access_count INTEGER, + access_count INTEGER NOT NULL, + + creation_date DATETIME NOT NULL, + revision_date DATETIME NOT NULL, + expiration_date DATETIME, + deletion_date DATETIME NOT NULL, + + disabled BOOLEAN NOT NULL +); \ No newline at end of file diff --git a/migrations/sqlite/2021-03-15-163412_rename_send_key/down.sql b/migrations/sqlite/2021-03-15-163412_rename_send_key/down.sql new file mode 100644 index 00000000..e69de29b diff --git a/migrations/sqlite/2021-03-15-163412_rename_send_key/up.sql b/migrations/sqlite/2021-03-15-163412_rename_send_key/up.sql new file mode 100644 index 00000000..08339fac --- /dev/null +++ b/migrations/sqlite/2021-03-15-163412_rename_send_key/up.sql @@ -0,0 +1 @@ +ALTER TABLE sends RENAME COLUMN key TO akey; diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index a882be97..fe815401 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -104,6 +104,12 @@ fn sync(data: Form, headers: Headers, conn: DbConn) -> JsonResult { .map(|c| c.to_json(&headers.host, &headers.user.uuid, &conn)) .collect(); + let sends = Send::find_by_user(&headers.user.uuid, &conn); + let sends_json: Vec = sends + .iter() + .map(|s| s.to_json()) + .collect(); + let domains_json = if data.exclude_domains { Value::Null } else { @@ -117,6 +123,7 @@ fn sync(data: Form, headers: Headers, conn: DbConn) -> JsonResult { "Policies": policies_json, "Ciphers": ciphers_json, "Domains": domains_json, + "Sends": sends_json, "Object": "sync" }))) } diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index a2af79a5..4f0c19c9 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -3,6 +3,7 @@ mod ciphers; mod folders; mod organizations; pub mod two_factor; +mod sends; pub fn routes() -> Vec { let mut mod_routes = routes![ @@ -20,6 +21,7 @@ pub fn routes() -> Vec { routes.append(&mut folders::routes()); routes.append(&mut organizations::routes()); routes.append(&mut two_factor::routes()); + routes.append(&mut sends::routes()); routes.append(&mut mod_routes); routes diff --git a/src/api/core/sends.rs b/src/api/core/sends.rs new file mode 100644 index 00000000..b69c4309 --- /dev/null +++ b/src/api/core/sends.rs @@ -0,0 +1,383 @@ +use std::{io::Read, path::Path}; + +use chrono::{DateTime, Duration, Utc}; +use multipart::server::{save::SavedData, Multipart, SaveResult}; +use rocket::{http::ContentType, Data}; +use rocket_contrib::json::Json; +use serde_json::Value; + +use crate::{ + api::{ApiResult, EmptyResult, JsonResult, JsonUpcase, Notify, UpdateType}, + auth::{Headers, Host}, + db::{models::*, DbConn}, + CONFIG, +}; + +pub fn routes() -> Vec { + routes![ + post_send, + post_send_file, + post_access, + post_access_file, + put_send, + delete_send, + put_remove_password + ] +} + +#[derive(Deserialize)] +#[allow(non_snake_case)] +pub struct SendData { + pub Type: i32, + pub Key: String, + pub Password: Option, + pub MaxAccessCount: Option, + pub ExpirationDate: Option>, + pub DeletionDate: DateTime, + pub Disabled: bool, + + // Data field + pub Name: String, + pub Notes: Option, + pub Text: Option, + pub File: Option, +} + +fn create_send(data: SendData, user_uuid: String) -> ApiResult { + let data_val = if data.Type == SendType::Text as i32 { + data.Text + } else if data.Type == SendType::File as i32 { + data.File + } else { + err!("Invalid Send type") + }; + + let data_str = if let Some(mut d) = data_val { + d.as_object_mut().and_then(|o| o.remove("Response")); + serde_json::to_string(&d)? + } else { + err!("Send data not provided"); + }; + + if data.DeletionDate > Utc::now() + Duration::days(31) { + err!( + "You cannot have a Send with a deletion date that far into the future. Adjust the Deletion Date to a value less than 31 days from now and try again." + ); + } + + let mut send = Send::new(data.Type, data.Name, data_str, data.Key, data.DeletionDate.naive_utc()); + send.user_uuid = Some(user_uuid); + send.notes = data.Notes; + send.max_access_count = data.MaxAccessCount; + send.expiration_date = data.ExpirationDate.map(|d| d.naive_utc()); + send.disabled = data.Disabled; + send.atype = data.Type; + + send.set_password(data.Password.as_deref()); + + Ok(send) +} + +#[post("/sends", data = "")] +fn post_send(data: JsonUpcase, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult { + let data: SendData = data.into_inner().data; + + if data.Type == SendType::File as i32 { + err!("File sends should use /api/sends/file") + } + + let mut send = create_send(data, headers.user.uuid.clone())?; + send.save(&conn)?; + nt.send_user_update(UpdateType::SyncSendCreate, &headers.user); + + Ok(Json(send.to_json())) +} + +#[post("/sends/file", format = "multipart/form-data", data = "")] +fn post_send_file(data: Data, content_type: &ContentType, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult { + let boundary = content_type.params().next().expect("No boundary provided").1; + + let mut mpart = Multipart::with_body(data.open(), boundary); + + // First entry is the SendData JSON + let mut model_entry = match mpart.read_entry()? { + Some(e) if &*e.headers.name == "model" => e, + Some(_) => err!("Invalid entry name"), + None => err!("No model entry present"), + }; + + let mut buf = String::new(); + model_entry.data.read_to_string(&mut buf)?; + let data = serde_json::from_str::>(&buf)?; + + // Get the file length and add an extra 10% to avoid issues + const SIZE_110_MB: u64 = 115_343_360; + + let size_limit = match CONFIG.user_attachment_limit() { + Some(0) => err!("File uploads are disabled"), + Some(limit_kb) => { + let left = (limit_kb * 1024) - Attachment::size_by_user(&headers.user.uuid, &conn); + if left <= 0 { + err!("Attachment size limit reached! Delete some files to open space") + } + std::cmp::Ord::max(left as u64, SIZE_110_MB) + } + None => SIZE_110_MB, + }; + + // Create the Send + let mut send = create_send(data.data, headers.user.uuid.clone())?; + let file_id: String = data_encoding::HEXLOWER.encode(&crate::crypto::get_random(vec![0; 32])); + + if send.atype != SendType::File as i32 { + err!("Send content is not a file"); + } + + let file_path = Path::new(&CONFIG.sends_folder()).join(&send.uuid).join(&file_id); + + // Read the data entry and save the file + let mut data_entry = match mpart.read_entry()? { + Some(e) if &*e.headers.name == "data" => e, + Some(_) => err!("Invalid entry name"), + None => err!("No model entry present"), + }; + + let size = match data_entry + .data + .save() + .memory_threshold(0) + .size_limit(size_limit) + .with_path(&file_path) + { + SaveResult::Full(SavedData::File(_, size)) => size as i32, + SaveResult::Full(other) => { + std::fs::remove_file(&file_path).ok(); + err!(format!("Attachment is not a file: {:?}", other)); + } + SaveResult::Partial(_, reason) => { + std::fs::remove_file(&file_path).ok(); + err!(format!("Attachment size limit exceeded with this file: {:?}", reason)); + } + SaveResult::Error(e) => { + std::fs::remove_file(&file_path).ok(); + err!(format!("Error: {:?}", e)); + } + }; + + // Set ID and sizes + let mut data_value: Value = serde_json::from_str(&send.data)?; + if let Some(o) = data_value.as_object_mut() { + o.insert(String::from("Id"), Value::String(file_id)); + o.insert(String::from("Size"), Value::Number(size.into())); + o.insert( + String::from("SizeName"), + Value::String(crate::util::get_display_size(size)), + ); + } + send.data = serde_json::to_string(&data_value)?; + + // Save the changes in the database + send.save(&conn)?; + nt.send_user_update(UpdateType::SyncSendCreate, &headers.user); + + Ok(Json(send.to_json())) +} + +#[derive(Deserialize)] +#[allow(non_snake_case)] +pub struct SendAccessData { + pub Password: Option, +} + +#[post("/sends/access/", data = "")] +fn post_access(access_id: String, data: JsonUpcase, conn: DbConn) -> JsonResult { + let mut send = match Send::find_by_access_id(&access_id, &conn) { + Some(s) => s, + None => err_code!("Send not found", 404), + }; + + if let Some(max_access_count) = send.max_access_count { + if send.access_count >= max_access_count { + err_code!("Max access count reached", 404); + } + } + + if let Some(expiration) = send.expiration_date { + if Utc::now().naive_utc() >= expiration { + err_code!("Send has expired", 404) + } + } + + if Utc::now().naive_utc() >= send.deletion_date { + err_code!("Send has been deleted", 404) + } + + if send.disabled { + err_code!("Send has been disabled", 404) + } + + if send.password_hash.is_some() { + match data.into_inner().data.Password { + Some(ref p) if send.check_password(p) => { /* Nothing to do here */ } + Some(_) => err!("Invalid password."), + None => err_code!("Password not provided", 401), + } + } + + // Files are incremented during the download + if send.atype == SendType::Text as i32 { + send.access_count += 1; + } + + send.save(&conn)?; + + Ok(Json(send.to_json())) +} + +#[post("/sends//access/file/", data = "")] +fn post_access_file( + send_id: String, + file_id: String, + data: JsonUpcase, + host: Host, + conn: DbConn, +) -> JsonResult { + let mut send = match Send::find_by_uuid(&send_id, &conn) { + Some(s) => s, + None => err_code!("Send not found", 404), + }; + + if let Some(max_access_count) = send.max_access_count { + if send.access_count >= max_access_count { + err_code!("Max access count reached", 404); + } + } + + if let Some(expiration) = send.expiration_date { + if Utc::now().naive_utc() >= expiration { + err_code!("Send has expired", 404) + } + } + + if Utc::now().naive_utc() >= send.deletion_date { + err_code!("Send has been deleted", 404) + } + + if send.disabled { + err_code!("Send has been disabled", 404) + } + + if send.password_hash.is_some() { + match data.into_inner().data.Password { + Some(ref p) if send.check_password(p) => { /* Nothing to do here */ } + Some(_) => err!("Invalid password."), + None => err_code!("Password not provided", 401), + } + } + + send.access_count += 1; + + send.save(&conn)?; + + Ok(Json(json!({ + "Object": "send-fileDownload", + "Id": file_id, + "Url": format!("{}/sends/{}/{}", &host.host, send_id, file_id) + }))) +} + +#[put("/sends/", data = "")] +fn put_send(id: String, data: JsonUpcase, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult { + let data: SendData = data.into_inner().data; + + let mut send = match Send::find_by_uuid(&id, &conn) { + Some(s) => s, + None => err!("Send not found"), + }; + + if send.user_uuid.as_ref() != Some(&headers.user.uuid) { + err!("Send is not owned by user") + } + + if send.atype != data.Type { + err!("Sends can't change type") + } + + let data_val = if data.Type == SendType::Text as i32 { + data.Text + } else if data.Type == SendType::File as i32 { + data.File + } else { + err!("Invalid Send type") + }; + + let data_str = if let Some(mut d) = data_val { + d.as_object_mut().and_then(|d| d.remove("Response")); + serde_json::to_string(&d)? + } else { + err!("Send data not provided"); + }; + + if data.DeletionDate > Utc::now() + Duration::days(31) { + err!( + "You cannot have a Send with a deletion date that far into the future. Adjust the Deletion Date to a value less than 31 days from now and try again." + ); + } + send.data = data_str; + send.name = data.Name; + send.akey = data.Key; + send.deletion_date = data.DeletionDate.naive_utc(); + send.notes = data.Notes; + send.max_access_count = data.MaxAccessCount; + send.expiration_date = data.ExpirationDate.map(|d| d.naive_utc()); + send.disabled = data.Disabled; + + // Only change the value if it's present + if let Some(password) = data.Password { + send.set_password(Some(&password)); + } + + send.save(&conn)?; + nt.send_user_update(UpdateType::SyncSendUpdate, &headers.user); + + Ok(Json(send.to_json())) +} + +#[delete("/sends/")] +fn delete_send(id: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult { + let send = match Send::find_by_uuid(&id, &conn) { + Some(s) => s, + None => err!("Send not found"), + }; + + if send.user_uuid.as_ref() != Some(&headers.user.uuid) { + err!("Send is not owned by user") + } + + if send.atype == SendType::File as i32 { + std::fs::remove_dir_all(Path::new(&CONFIG.sends_folder()).join(&send.uuid)).ok(); + } + + send.delete(&conn)?; + nt.send_user_update(UpdateType::SyncSendDelete, &headers.user); + + Ok(()) +} + +#[put("/sends//remove-password")] +fn put_remove_password(id: String, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult { + let mut send = match Send::find_by_uuid(&id, &conn) { + Some(s) => s, + None => err!("Send not found"), + }; + + if send.user_uuid.as_ref() != Some(&headers.user.uuid) { + err!("Send is not owned by user") + } + + send.set_password(None); + send.save(&conn)?; + nt.send_user_update(UpdateType::SyncSendUpdate, &headers.user); + + Ok(Json(send.to_json())) +} diff --git a/src/api/notifications.rs b/src/api/notifications.rs index 2e81b620..3a6423c1 100644 --- a/src/api/notifications.rs +++ b/src/api/notifications.rs @@ -394,6 +394,10 @@ pub enum UpdateType { LogOut = 11, + SyncSendCreate = 12, + SyncSendUpdate = 13, + SyncSendDelete = 14, + None = 100, } diff --git a/src/api/web.rs b/src/api/web.rs index 7956c674..90e572a8 100644 --- a/src/api/web.rs +++ b/src/api/web.rs @@ -10,7 +10,7 @@ pub fn routes() -> Vec { // If addding more routes here, consider also adding them to // crate::utils::LOGGED_ROUTES to make sure they appear in the log if CONFIG.web_vault_enabled() { - routes![web_index, app_id, web_files, attachments, alive, static_files] + routes![web_index, app_id, web_files, attachments, sends, alive, static_files] } else { routes![attachments, alive, static_files] } @@ -60,6 +60,11 @@ fn attachments(uuid: String, file: PathBuf) -> Option { NamedFile::open(Path::new(&CONFIG.attachments_folder()).join(uuid).join(file)).ok() } +#[get("/sends//")] +fn sends(send_id: String, file_id: String) -> Option { + NamedFile::open(Path::new(&CONFIG.sends_folder()).join(send_id).join(file_id)).ok() +} + #[get("/alive")] fn alive() -> Json { use crate::util::format_date; diff --git a/src/auth.rs b/src/auth.rs index eaf4c707..4041cc6b 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -222,13 +222,12 @@ use crate::db::{ DbConn, }; -pub struct Headers { - pub host: String, - pub device: Device, - pub user: User, +pub struct Host { + pub host: String } -impl<'a, 'r> FromRequest<'a, 'r> for Headers { + +impl<'a, 'r> FromRequest<'a, 'r> for Host { type Error = &'static str; fn from_request(request: &'a Request<'r>) -> Outcome { @@ -262,6 +261,28 @@ impl<'a, 'r> FromRequest<'a, 'r> for Headers { format!("{}://{}", protocol, host) }; + Outcome::Success(Host { host }) + } +} + +pub struct Headers { + pub host: String, + pub device: Device, + pub user: User, +} + +impl<'a, 'r> FromRequest<'a, 'r> for Headers { + type Error = &'static str; + + fn from_request(request: &'a Request<'r>) -> Outcome { + let headers = request.headers(); + + let host = match Host::from_request(request) { + Outcome::Forward(_) => return Outcome::Forward(()), + Outcome::Failure(f) => return Outcome::Failure(f), + Outcome::Success(host) => host.host, + }; + // Get access_token let access_token: &str = match headers.get_one("Authorization") { Some(a) => match a.rsplit("Bearer ").next() { diff --git a/src/config.rs b/src/config.rs index 3f344f01..6c41c975 100644 --- a/src/config.rs +++ b/src/config.rs @@ -299,6 +299,8 @@ make_config! { icon_cache_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "icon_cache"); /// Attachments folder attachments_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "attachments"); + /// Sends folder + sends_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "sends"); /// Templates folder templates_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "templates"); /// Session JWT key diff --git a/src/db/models/attachment.rs b/src/db/models/attachment.rs index f89a6dbc..6a8fc5b0 100644 --- a/src/db/models/attachment.rs +++ b/src/db/models/attachment.rs @@ -4,7 +4,7 @@ use super::Cipher; use crate::CONFIG; db_object! { - #[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)] + #[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)] #[table_name = "attachments"] #[changeset_options(treat_none_as_null="true")] #[belongs_to(super::Cipher, foreign_key = "cipher_uuid")] diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs index d41ebfb8..da3eabab 100644 --- a/src/db/models/cipher.rs +++ b/src/db/models/cipher.rs @@ -14,7 +14,7 @@ use super::{ }; db_object! { - #[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)] + #[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)] #[table_name = "ciphers"] #[changeset_options(treat_none_as_null="true")] #[belongs_to(User, foreign_key = "user_uuid")] diff --git a/src/db/models/collection.rs b/src/db/models/collection.rs index 4e75279d..5fbc3128 100644 --- a/src/db/models/collection.rs +++ b/src/db/models/collection.rs @@ -3,7 +3,7 @@ use serde_json::Value; use super::{Organization, UserOrgStatus, UserOrgType, UserOrganization, User, Cipher}; db_object! { - #[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)] + #[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)] #[table_name = "collections"] #[belongs_to(Organization, foreign_key = "org_uuid")] #[primary_key(uuid)] @@ -13,7 +13,7 @@ db_object! { pub name: String, } - #[derive(Debug, Identifiable, Queryable, Insertable, Associations)] + #[derive(Identifiable, Queryable, Insertable, Associations)] #[table_name = "users_collections"] #[belongs_to(User, foreign_key = "user_uuid")] #[belongs_to(Collection, foreign_key = "collection_uuid")] @@ -25,7 +25,7 @@ db_object! { pub hide_passwords: bool, } - #[derive(Debug, Identifiable, Queryable, Insertable, Associations)] + #[derive(Identifiable, Queryable, Insertable, Associations)] #[table_name = "ciphers_collections"] #[belongs_to(Cipher, foreign_key = "cipher_uuid")] #[belongs_to(Collection, foreign_key = "collection_uuid")] diff --git a/src/db/models/device.rs b/src/db/models/device.rs index 0172ffb5..b3413ada 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -4,7 +4,7 @@ use super::User; use crate::CONFIG; db_object! { - #[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)] + #[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)] #[table_name = "devices"] #[changeset_options(treat_none_as_null="true")] #[belongs_to(User, foreign_key = "user_uuid")] diff --git a/src/db/models/favorite.rs b/src/db/models/favorite.rs index f419e07d..4f610f21 100644 --- a/src/db/models/favorite.rs +++ b/src/db/models/favorite.rs @@ -1,7 +1,7 @@ use super::{Cipher, User}; db_object! { - #[derive(Debug, Identifiable, Queryable, Insertable, Associations)] + #[derive(Identifiable, Queryable, Insertable, Associations)] #[table_name = "favorites"] #[belongs_to(User, foreign_key = "user_uuid")] #[belongs_to(Cipher, foreign_key = "cipher_uuid")] diff --git a/src/db/models/folder.rs b/src/db/models/folder.rs index 3656afb3..b5bbcc79 100644 --- a/src/db/models/folder.rs +++ b/src/db/models/folder.rs @@ -4,7 +4,7 @@ use serde_json::Value; use super::{Cipher, User}; db_object! { - #[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)] + #[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)] #[table_name = "folders"] #[belongs_to(User, foreign_key = "user_uuid")] #[primary_key(uuid)] @@ -16,7 +16,7 @@ db_object! { pub name: String, } - #[derive(Debug, Identifiable, Queryable, Insertable, Associations)] + #[derive(Identifiable, Queryable, Insertable, Associations)] #[table_name = "folders_ciphers"] #[belongs_to(Cipher, foreign_key = "cipher_uuid")] #[belongs_to(Folder, foreign_key = "folder_uuid")] diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs index 8c886fb4..a4fb635b 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -8,6 +8,7 @@ mod org_policy; mod organization; mod two_factor; mod user; +mod send; pub use self::attachment::Attachment; pub use self::cipher::Cipher; @@ -19,3 +20,4 @@ pub use self::org_policy::{OrgPolicy, OrgPolicyType}; pub use self::organization::{Organization, UserOrgStatus, UserOrgType, UserOrganization}; pub use self::two_factor::{TwoFactor, TwoFactorType}; pub use self::user::{Invitation, User, UserStampException}; +pub use self::send::{Send, SendType}; \ No newline at end of file diff --git a/src/db/models/org_policy.rs b/src/db/models/org_policy.rs index 679b7e57..65569b48 100644 --- a/src/db/models/org_policy.rs +++ b/src/db/models/org_policy.rs @@ -7,7 +7,7 @@ use crate::error::MapResult; use super::{Organization, UserOrgStatus}; db_object! { - #[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)] + #[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)] #[table_name = "org_policies"] #[belongs_to(Organization, foreign_key = "org_uuid")] #[primary_key(uuid)] diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs index ade0040b..2e93426d 100644 --- a/src/db/models/organization.rs +++ b/src/db/models/organization.rs @@ -5,7 +5,7 @@ use num_traits::FromPrimitive; use super::{CollectionUser, User, OrgPolicy}; db_object! { - #[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset)] + #[derive(Identifiable, Queryable, Insertable, AsChangeset)] #[table_name = "organizations"] #[primary_key(uuid)] pub struct Organization { @@ -14,7 +14,7 @@ db_object! { pub billing_email: String, } - #[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset)] + #[derive(Identifiable, Queryable, Insertable, AsChangeset)] #[table_name = "users_organizations"] #[primary_key(uuid)] pub struct UserOrganization { diff --git a/src/db/models/send.rs b/src/db/models/send.rs new file mode 100644 index 00000000..d7a7a872 --- /dev/null +++ b/src/db/models/send.rs @@ -0,0 +1,235 @@ +use chrono::{NaiveDateTime, Utc}; +use serde_json::Value; + +use super::{Organization, User}; + +db_object! { + #[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)] + #[table_name = "sends"] + #[changeset_options(treat_none_as_null="true")] + #[belongs_to(User, foreign_key = "user_uuid")] + #[belongs_to(Organization, foreign_key = "organization_uuid")] + #[primary_key(uuid)] + pub struct Send { + pub uuid: String, + + pub user_uuid: Option, + pub organization_uuid: Option, + + + pub name: String, + pub notes: Option, + + pub atype: i32, + pub data: String, + pub akey: String, + pub password_hash: Option>, + password_salt: Option>, + password_iter: Option, + + pub max_access_count: Option, + pub access_count: i32, + + pub creation_date: NaiveDateTime, + pub revision_date: NaiveDateTime, + pub expiration_date: Option, + pub deletion_date: NaiveDateTime, + + pub disabled: bool, + } +} + +#[derive(Copy, Clone, PartialEq, Eq, num_derive::FromPrimitive)] +pub enum SendType { + Text = 0, + File = 1, +} + +impl Send { + pub fn new(atype: i32, name: String, data: String, akey: String, deletion_date: NaiveDateTime) -> Self { + let now = Utc::now().naive_utc(); + + Self { + uuid: crate::util::get_uuid(), + user_uuid: None, + organization_uuid: None, + + name, + notes: None, + + atype, + data, + akey, + password_hash: None, + password_salt: None, + password_iter: None, + + max_access_count: None, + access_count: 0, + + creation_date: now, + revision_date: now, + expiration_date: None, + deletion_date, + + disabled: false, + } + } + + pub fn set_password(&mut self, password: Option<&str>) { + const PASSWORD_ITER: i32 = 100_000; + + if let Some(password) = password { + self.password_iter = Some(PASSWORD_ITER); + let salt = crate::crypto::get_random_64(); + let hash = crate::crypto::hash_password(password.as_bytes(), &salt, PASSWORD_ITER as u32); + self.password_salt = Some(salt); + self.password_hash = Some(hash); + } else { + self.password_iter = None; + self.password_salt = None; + self.password_hash = None; + } + } + + pub fn check_password(&self, password: &str) -> bool { + match (&self.password_hash, &self.password_salt, self.password_iter) { + (Some(hash), Some(salt), Some(iter)) => { + crate::crypto::verify_password_hash(password.as_bytes(), salt, hash, iter as u32) + } + _ => false, + } + } + + pub fn to_json(&self) -> Value { + use crate::util::format_date; + use data_encoding::BASE64URL_NOPAD; + use uuid::Uuid; + + let data: Value = serde_json::from_str(&self.data).unwrap_or_default(); + + json!({ + "Id": self.uuid, + "AccessId": BASE64URL_NOPAD.encode(Uuid::parse_str(&self.uuid).unwrap_or_default().as_bytes()), + "Type": self.atype, + + "Name": self.name, + "Notes": self.notes, + "Text": if self.atype == SendType::Text as i32 { Some(&data) } else { None }, + "File": if self.atype == SendType::File as i32 { Some(&data) } else { None }, + + "Key": self.akey, + "MaxAccessCount": self.max_access_count, + "AccessCount": self.access_count, + "Password": self.password_hash.as_deref().map(|h| BASE64URL_NOPAD.encode(h)), + "Disabled": self.disabled, + + "RevisionDate": format_date(&self.revision_date), + "ExpirationDate": self.expiration_date.as_ref().map(format_date), + "DeletionDate": format_date(&self.deletion_date), + "Object": "send", + }) + } +} + +use crate::db::DbConn; + +use crate::api::EmptyResult; +use crate::error::MapResult; + +impl Send { + pub fn save(&mut self, conn: &DbConn) -> EmptyResult { + // self.update_users_revision(conn); + self.revision_date = Utc::now().naive_utc(); + + db_run! { conn: + sqlite, mysql { + match diesel::replace_into(sends::table) + .values(SendDb::to_db(self)) + .execute(conn) + { + Ok(_) => Ok(()), + // Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first. + Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { + diesel::update(sends::table) + .filter(sends::uuid.eq(&self.uuid)) + .set(SendDb::to_db(self)) + .execute(conn) + .map_res("Error saving send") + } + Err(e) => Err(e.into()), + }.map_res("Error saving send") + } + postgresql { + let value = SendDb::to_db(self); + diesel::insert_into(sends::table) + .values(&value) + .on_conflict(sends::uuid) + .do_update() + .set(&value) + .execute(conn) + .map_res("Error saving send") + } + } + } + + pub fn delete(&self, conn: &DbConn) -> EmptyResult { + // self.update_users_revision(conn); + + db_run! { conn: { + diesel::delete(sends::table.filter(sends::uuid.eq(&self.uuid))) + .execute(conn) + .map_res("Error deleting send") + }} + } + + pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult { + for send in Self::find_by_user(user_uuid, &conn) { + send.delete(&conn)?; + } + Ok(()) + } + + pub fn find_by_access_id(access_id: &str, conn: &DbConn) -> Option { + use data_encoding::BASE64URL_NOPAD; + use uuid::Uuid; + + let uuid_vec = match BASE64URL_NOPAD.decode(access_id.as_bytes()) { + Ok(v) => v, + Err(_) => return None, + }; + + let uuid = match Uuid::from_slice(&uuid_vec) { + Ok(u) => u.to_string(), + Err(_) => return None, + }; + + Self::find_by_uuid(&uuid, conn) + } + + pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option { + db_run! {conn: { + sends::table + .filter(sends::uuid.eq(uuid)) + .first::(conn) + .ok() + .from_db() + }} + } + + pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec { + db_run! {conn: { + sends::table + .filter(sends::user_uuid.eq(user_uuid)) + .load::(conn).expect("Error loading sends").from_db() + }} + } + + pub fn find_by_org(org_uuid: &str, conn: &DbConn) -> Vec { + db_run! {conn: { + sends::table + .filter(sends::organization_uuid.eq(org_uuid)) + .load::(conn).expect("Error loading sends").from_db() + }} + } +} diff --git a/src/db/models/two_factor.rs b/src/db/models/two_factor.rs index 79a35c19..18073bad 100644 --- a/src/db/models/two_factor.rs +++ b/src/db/models/two_factor.rs @@ -7,7 +7,7 @@ use crate::error::MapResult; use super::User; db_object! { - #[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)] + #[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)] #[table_name = "twofactor"] #[belongs_to(User, foreign_key = "user_uuid")] #[primary_key(uuid)] diff --git a/src/db/models/user.rs b/src/db/models/user.rs index eab46e51..fdd2dcae 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -5,7 +5,7 @@ use crate::crypto; use crate::CONFIG; db_object! { - #[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset)] + #[derive(Identifiable, Queryable, Insertable, AsChangeset)] #[table_name = "users"] #[changeset_options(treat_none_as_null="true")] #[primary_key(uuid)] @@ -47,7 +47,7 @@ db_object! { } - #[derive(Debug, Identifiable, Queryable, Insertable)] + #[derive(Identifiable, Queryable, Insertable)] #[table_name = "invitations"] #[primary_key(email)] pub struct Invitation { @@ -177,7 +177,7 @@ impl User { } } -use super::{Cipher, Device, Favorite, Folder, TwoFactor, UserOrgType, UserOrganization}; +use super::{Cipher, Device, Favorite, Folder, Send, TwoFactor, UserOrgType, UserOrganization}; use crate::db::DbConn; use crate::api::EmptyResult; @@ -263,6 +263,7 @@ impl User { } } + Send::delete_all_by_user(&self.uuid, conn)?; UserOrganization::delete_all_by_user(&self.uuid, conn)?; Cipher::delete_all_by_user(&self.uuid, conn)?; Favorite::delete_all_by_user(&self.uuid, conn)?; diff --git a/src/db/schemas/mysql/schema.rs b/src/db/schemas/mysql/schema.rs index d1a9962a..346b2959 100644 --- a/src/db/schemas/mysql/schema.rs +++ b/src/db/schemas/mysql/schema.rs @@ -102,6 +102,29 @@ table! { } } +table! { + sends (uuid) { + uuid -> Text, + user_uuid -> Nullable, + organization_uuid -> Nullable, + name -> Text, + notes -> Nullable, + atype -> Integer, + data -> Text, + akey -> Text, + password_hash -> Nullable, + password_salt -> Nullable, + password_iter -> Nullable, + max_access_count -> Nullable, + access_count -> Integer, + creation_date -> Datetime, + revision_date -> Datetime, + expiration_date -> Nullable, + deletion_date -> Datetime, + disabled -> Bool, + } +} + table! { twofactor (uuid) { uuid -> Text, @@ -176,6 +199,8 @@ joinable!(folders -> users (user_uuid)); joinable!(folders_ciphers -> ciphers (cipher_uuid)); joinable!(folders_ciphers -> folders (folder_uuid)); joinable!(org_policies -> organizations (org_uuid)); +joinable!(sends -> organizations (organization_uuid)); +joinable!(sends -> users (user_uuid)); joinable!(twofactor -> users (user_uuid)); joinable!(users_collections -> collections (collection_uuid)); joinable!(users_collections -> users (user_uuid)); @@ -193,6 +218,7 @@ allow_tables_to_appear_in_same_query!( invitations, org_policies, organizations, + sends, twofactor, users, users_collections, diff --git a/src/db/schemas/postgresql/schema.rs b/src/db/schemas/postgresql/schema.rs index 5af0ece6..f8e1e9fe 100644 --- a/src/db/schemas/postgresql/schema.rs +++ b/src/db/schemas/postgresql/schema.rs @@ -102,6 +102,29 @@ table! { } } +table! { + sends (uuid) { + uuid -> Text, + user_uuid -> Nullable, + organization_uuid -> Nullable, + name -> Text, + notes -> Nullable, + atype -> Integer, + data -> Text, + akey -> Text, + password_hash -> Nullable, + password_salt -> Nullable, + password_iter -> Nullable, + max_access_count -> Nullable, + access_count -> Integer, + creation_date -> Timestamp, + revision_date -> Timestamp, + expiration_date -> Nullable, + deletion_date -> Timestamp, + disabled -> Bool, + } +} + table! { twofactor (uuid) { uuid -> Text, @@ -176,6 +199,8 @@ joinable!(folders -> users (user_uuid)); joinable!(folders_ciphers -> ciphers (cipher_uuid)); joinable!(folders_ciphers -> folders (folder_uuid)); joinable!(org_policies -> organizations (org_uuid)); +joinable!(sends -> organizations (organization_uuid)); +joinable!(sends -> users (user_uuid)); joinable!(twofactor -> users (user_uuid)); joinable!(users_collections -> collections (collection_uuid)); joinable!(users_collections -> users (user_uuid)); @@ -193,6 +218,7 @@ allow_tables_to_appear_in_same_query!( invitations, org_policies, organizations, + sends, twofactor, users, users_collections, diff --git a/src/db/schemas/sqlite/schema.rs b/src/db/schemas/sqlite/schema.rs index 5af0ece6..f8e1e9fe 100644 --- a/src/db/schemas/sqlite/schema.rs +++ b/src/db/schemas/sqlite/schema.rs @@ -102,6 +102,29 @@ table! { } } +table! { + sends (uuid) { + uuid -> Text, + user_uuid -> Nullable, + organization_uuid -> Nullable, + name -> Text, + notes -> Nullable, + atype -> Integer, + data -> Text, + akey -> Text, + password_hash -> Nullable, + password_salt -> Nullable, + password_iter -> Nullable, + max_access_count -> Nullable, + access_count -> Integer, + creation_date -> Timestamp, + revision_date -> Timestamp, + expiration_date -> Nullable, + deletion_date -> Timestamp, + disabled -> Bool, + } +} + table! { twofactor (uuid) { uuid -> Text, @@ -176,6 +199,8 @@ joinable!(folders -> users (user_uuid)); joinable!(folders_ciphers -> ciphers (cipher_uuid)); joinable!(folders_ciphers -> folders (folder_uuid)); joinable!(org_policies -> organizations (org_uuid)); +joinable!(sends -> organizations (organization_uuid)); +joinable!(sends -> users (user_uuid)); joinable!(twofactor -> users (user_uuid)); joinable!(users_collections -> collections (collection_uuid)); joinable!(users_collections -> users (user_uuid)); @@ -193,6 +218,7 @@ allow_tables_to_appear_in_same_query!( invitations, org_policies, organizations, + sends, twofactor, users, users_collections, diff --git a/src/error.rs b/src/error.rs index 80f9dafc..fa90f1d3 100644 --- a/src/error.rs +++ b/src/error.rs @@ -220,6 +220,18 @@ macro_rules! err { }}; } +#[macro_export] +macro_rules! err_code { + ($msg:expr, $err_code: literal) => {{ + error!("{}", $msg); + return Err(crate::error::Error::new($msg, $msg).with_code($err_code)); + }}; + ($usr_msg:expr, $log_value:expr, $err_code: literal) => {{ + error!("{}. {}", $usr_msg, $log_value); + return Err(crate::error::Error::new($usr_msg, $log_value).with_code($err_code)); + }}; +} + #[macro_export] macro_rules! err_discard { ($msg:expr, $data:expr) => {{