├── .discourse-compatibility
├── .github
└── workflows
│ └── discourse-plugin.yml
├── .gitignore
├── .npmrc
├── .prettierrc.cjs
├── .rubocop.yml
├── .streerc
├── .template-lintrc.cjs
├── Gemfile
├── Gemfile.lock
├── LICENSE
├── README.md
├── app
├── controllers
│ ├── encrypt_controller.rb
│ └── encrypted_post_timers_controller.rb
├── jobs
│ └── scheduled
│ │ ├── encrypt_consistency.rb
│ │ └── encrypted_post_timer_evaluator.rb
├── mailers
│ └── user_notifications_extensions.rb
├── models
│ ├── encrypted_post_timer.rb
│ ├── encrypted_topics_data.rb
│ ├── encrypted_topics_user.rb
│ └── user_encryption_key.rb
└── services
│ └── problem_check
│ └── unsafe_csp.rb
├── assets
├── javascripts
│ ├── discourse
│ │ ├── api-initializers
│ │ │ ├── encrypt-delete-topic.js
│ │ │ ├── encrypt-icons.gjs
│ │ │ └── encrypt-model-transformers.js
│ │ ├── components
│ │ │ ├── encrypt-enable-dropdown.js
│ │ │ ├── encrypt-preferences-dropdown.js
│ │ │ ├── encrypted-post-timer-dropdown.js
│ │ │ └── modal
│ │ │ │ ├── activate-encrypt.hbs
│ │ │ │ ├── activate-encrypt.js
│ │ │ │ ├── decrypt-all.gjs
│ │ │ │ ├── export-key-pair.hbs
│ │ │ │ ├── export-key-pair.js
│ │ │ │ ├── generate-paper-key.hbs
│ │ │ │ ├── generate-paper-key.js
│ │ │ │ ├── manage-paper-keys.hbs
│ │ │ │ ├── manage-paper-keys.js
│ │ │ │ ├── reset-key-pair.hbs
│ │ │ │ ├── reset-key-pair.js
│ │ │ │ ├── rotate-key-pair.hbs
│ │ │ │ └── rotate-key-pair.js
│ │ ├── connectors
│ │ │ ├── composer-action-after
│ │ │ │ ├── encrypt.hbs
│ │ │ │ └── encrypt.js
│ │ │ ├── topic-above-post-stream
│ │ │ │ └── encrypt-topic-class.gjs
│ │ │ ├── topic-title
│ │ │ │ └── decrypt-topic-button.gjs
│ │ │ └── user-preferences-security
│ │ │ │ ├── encrypt.hbs
│ │ │ │ └── encrypt.js
│ │ ├── initializers
│ │ │ ├── add-search-results.js
│ │ │ ├── auto-enable-encrypt.js
│ │ │ ├── decrypt-post-revision.js
│ │ │ ├── decrypt-posts.js
│ │ │ ├── encrypt-composer.js
│ │ │ ├── encrypt-drafts.js
│ │ │ ├── encrypt-posts.js
│ │ │ ├── encrypt-status.js
│ │ │ ├── encrypt-uploads.js
│ │ │ ├── encrypt-user-options.js
│ │ │ ├── fetch-encrypt-keys.js
│ │ │ └── invite-to-encrypted-topic.js
│ │ ├── services
│ │ │ └── encrypt-widget-store.js
│ │ └── widgets
│ │ │ └── encrypted-post-timer-counter.js
│ └── lib
│ │ ├── base64.js
│ │ ├── database.js
│ │ ├── debounced-queue.js
│ │ ├── discourse.js
│ │ ├── pack.js
│ │ ├── paper-key.js
│ │ ├── permanent-topic-decrypter.js
│ │ ├── protocol-v0.js
│ │ ├── protocol-v1.js
│ │ ├── protocol.js
│ │ ├── uploads.js
│ │ ├── uppy-upload-encrypt-plugin.js
│ │ └── utils.js
└── stylesheets
│ ├── colors.scss
│ └── common
│ └── encrypt.scss
├── config
├── locales
│ ├── client.ar.yml
│ ├── client.be.yml
│ ├── client.bg.yml
│ ├── client.bs_BA.yml
│ ├── client.ca.yml
│ ├── client.cs.yml
│ ├── client.da.yml
│ ├── client.de.yml
│ ├── client.el.yml
│ ├── client.en.yml
│ ├── client.en_GB.yml
│ ├── client.es.yml
│ ├── client.et.yml
│ ├── client.fa_IR.yml
│ ├── client.fi.yml
│ ├── client.fr.yml
│ ├── client.gl.yml
│ ├── client.he.yml
│ ├── client.hr.yml
│ ├── client.hu.yml
│ ├── client.hy.yml
│ ├── client.id.yml
│ ├── client.it.yml
│ ├── client.ja.yml
│ ├── client.ko.yml
│ ├── client.lt.yml
│ ├── client.lv.yml
│ ├── client.nb_NO.yml
│ ├── client.nl.yml
│ ├── client.pl_PL.yml
│ ├── client.pt.yml
│ ├── client.pt_BR.yml
│ ├── client.ro.yml
│ ├── client.ru.yml
│ ├── client.sk.yml
│ ├── client.sl.yml
│ ├── client.sq.yml
│ ├── client.sr.yml
│ ├── client.sv.yml
│ ├── client.sw.yml
│ ├── client.te.yml
│ ├── client.th.yml
│ ├── client.tr_TR.yml
│ ├── client.ug.yml
│ ├── client.uk.yml
│ ├── client.ur.yml
│ ├── client.vi.yml
│ ├── client.zh_CN.yml
│ ├── client.zh_TW.yml
│ ├── server.ar.yml
│ ├── server.be.yml
│ ├── server.bg.yml
│ ├── server.bs_BA.yml
│ ├── server.ca.yml
│ ├── server.cs.yml
│ ├── server.da.yml
│ ├── server.de.yml
│ ├── server.el.yml
│ ├── server.en.yml
│ ├── server.en_GB.yml
│ ├── server.es.yml
│ ├── server.et.yml
│ ├── server.fa_IR.yml
│ ├── server.fi.yml
│ ├── server.fr.yml
│ ├── server.gl.yml
│ ├── server.he.yml
│ ├── server.hr.yml
│ ├── server.hu.yml
│ ├── server.hy.yml
│ ├── server.id.yml
│ ├── server.it.yml
│ ├── server.ja.yml
│ ├── server.ko.yml
│ ├── server.lt.yml
│ ├── server.lv.yml
│ ├── server.nb_NO.yml
│ ├── server.nl.yml
│ ├── server.pl_PL.yml
│ ├── server.pt.yml
│ ├── server.pt_BR.yml
│ ├── server.ro.yml
│ ├── server.ru.yml
│ ├── server.sk.yml
│ ├── server.sl.yml
│ ├── server.sq.yml
│ ├── server.sr.yml
│ ├── server.sv.yml
│ ├── server.sw.yml
│ ├── server.te.yml
│ ├── server.th.yml
│ ├── server.tr_TR.yml
│ ├── server.ug.yml
│ ├── server.uk.yml
│ ├── server.ur.yml
│ ├── server.vi.yml
│ ├── server.zh_CN.yml
│ └── server.zh_TW.yml
└── settings.yml
├── db
└── migrate
│ ├── 20190803111542_update_protocol.rb
│ ├── 20190916115531_add_paper_keys.rb
│ ├── 20200129230327_create_encrypted_topics_users.rb
│ ├── 20200130050409_create_user_encryption_keys.rb
│ ├── 20200223214818_create_encrypted_topics_data.rb
│ ├── 20201027233335_create_encrypted_post_timers.rb
│ ├── 20221102045508_add_encrypt_pms_default_to_user_options.rb
│ └── 20221123235603_allow_encrypt_pms_default_to_be_null.rb
├── eslint.config.mjs
├── lib
├── encrypted_post_creator.rb
├── encrypted_search.rb
├── grouped_search_result_serializer_extension.rb
├── openssl.rb
├── post_actions_controller_extensions.rb
├── post_extensions.rb
├── site_setting_extensions.rb
├── site_settings_type_supervisor_extensions.rb
├── topic_extensions.rb
├── topic_guardian_extensions.rb
├── topic_view_serializer_extension.rb
├── topics_controller_extensions.rb
├── upload_validator_extensions.rb
├── user_extensions.rb
├── user_notification_renderer_extensions.rb
└── validators
│ └── encrypt_enabled_validator.rb
├── package.json
├── plugin.rb
├── pnpm-lock.yaml
├── spec
├── fixtures
│ ├── test_paper_key_1.txt
│ ├── test_paper_key_2.txt
│ ├── test_private_key_1.txt
│ ├── test_private_key_2.txt
│ ├── test_public_key_1.txt
│ └── test_public_key_2.txt
├── jobs
│ ├── encrypt_consistency_spec.rb
│ └── encrypted_post_timer_evaluator_spec.rb
├── lib
│ ├── email_sender_spec.rb
│ ├── encrypted_post_creator_spec.rb
│ ├── site_setting_extensions_spec.rb
│ └── upload_validator_spec.rb
├── models
│ ├── post_spec.rb
│ ├── topic_spec.rb
│ └── user_spec.rb
├── plugin_helper.rb
├── plugin_spec.rb
├── requests
│ ├── encrypt_controller_spec.rb
│ ├── encrypted_post_timers_controller_spec.rb
│ ├── posts_controller_spec.rb
│ └── topics_controller_spec.rb
├── serializers
│ ├── current_user_serialier_spec.rb
│ ├── topic_serializers_spec.rb
│ └── topic_view_serializer_spec.rb
├── services
│ └── problem_check
│ │ └── unsafe_csp_spec.rb
└── system
│ ├── decrypt_encrypted_topic_posts_spec.rb
│ ├── enable_encryption_spec.rb
│ └── permanent_decrypt_spec.rb
├── svg-icons
└── plugin-icons.svg
├── test
└── javascripts
│ ├── acceptance
│ └── encrypt-test.js
│ └── lib
│ ├── base64-test.js
│ ├── database-safari-test.js
│ ├── database-test.js
│ ├── protocol-test.js
│ ├── protocol-v0-test.js
│ ├── protocol-v1-test.js
│ └── uploads-test.js
└── translator.yml
/.discourse-compatibility:
--------------------------------------------------------------------------------
1 | < 3.4.0.beta4-dev: 804f5efe5651ab08f93fd6eb1d54971d98f40944
2 | < 3.4.0.beta2-dev: 9a9f6af455cb2497aa64cd32031aca020b497829
3 | < 3.4.0.beta1-dev: 14e0fb988ba91eaa514341575cdde8042b823c5b
4 | < 3.3.0.beta2-dev: 54a59be6176d2ce02a7aaae8c6b7cb57b1f4bfff
5 | < 3.3.0.beta1-dev: 9013a8ea8c7c7df55b36f329b3bb09b754a04cfe
6 | < 3.2.0.beta3-dev: 47dcd1ec558660065f5a4224d990f1188c88a368
7 | 3.1.0.beta5: e0eb21f7a97c54cfe40eb07b2c5ca53cde7b370a
8 | 3.1.0.beta3: 0f3c612bd1d274ba71362c423f16fd19a32bbd46
9 | 2.9.0.beta3: adef8c76aaeccaeb081a61bdd8ce7ab90ca41538
10 | 2.9.0.beta2: dc2b844e386351c778b097c7a4e0ec65726296ec
11 |
--------------------------------------------------------------------------------
/.github/workflows/discourse-plugin.yml:
--------------------------------------------------------------------------------
1 | name: Discourse Plugin
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 |
9 | jobs:
10 | ci:
11 | uses: discourse/.github/.github/workflows/discourse-plugin.yml@v1
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | /gems
3 | /auto_generated
4 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | engine-strict = true
2 | auto-install-peers = false
3 |
--------------------------------------------------------------------------------
/.prettierrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = require("@discourse/lint-configs/prettier");
2 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | inherit_gem:
2 | rubocop-discourse: stree-compat.yml
3 |
--------------------------------------------------------------------------------
/.streerc:
--------------------------------------------------------------------------------
1 | --print-width=100
2 | --plugins=plugin/trailing_comma
3 |
--------------------------------------------------------------------------------
/.template-lintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = require("@discourse/lint-configs/template-lint");
2 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | source "https://rubygems.org"
4 |
5 | group :development do
6 | gem "rubocop-discourse"
7 | gem "syntax_tree"
8 | end
9 |
--------------------------------------------------------------------------------
/Gemfile.lock:
--------------------------------------------------------------------------------
1 | GEM
2 | remote: https://rubygems.org/
3 | specs:
4 | activesupport (7.1.3.3)
5 | base64
6 | bigdecimal
7 | concurrent-ruby (~> 1.0, >= 1.0.2)
8 | connection_pool (>= 2.2.5)
9 | drb
10 | i18n (>= 1.6, < 2)
11 | minitest (>= 5.1)
12 | mutex_m
13 | tzinfo (~> 2.0)
14 | ast (2.4.2)
15 | base64 (0.2.0)
16 | bigdecimal (3.1.8)
17 | concurrent-ruby (1.2.3)
18 | connection_pool (2.4.1)
19 | drb (2.2.1)
20 | i18n (1.14.5)
21 | concurrent-ruby (~> 1.0)
22 | json (2.7.2)
23 | language_server-protocol (3.17.0.3)
24 | minitest (5.23.1)
25 | mutex_m (0.2.0)
26 | parallel (1.24.0)
27 | parser (3.3.1.0)
28 | ast (~> 2.4.1)
29 | racc
30 | prettier_print (1.2.1)
31 | racc (1.8.0)
32 | rack (3.0.11)
33 | rainbow (3.1.1)
34 | regexp_parser (2.9.2)
35 | rexml (3.3.9)
36 | rubocop (1.64.0)
37 | json (~> 2.3)
38 | language_server-protocol (>= 3.17.0)
39 | parallel (~> 1.10)
40 | parser (>= 3.3.0.2)
41 | rainbow (>= 2.2.2, < 4.0)
42 | regexp_parser (>= 1.8, < 3.0)
43 | rexml (>= 3.2.5, < 4.0)
44 | rubocop-ast (>= 1.31.1, < 2.0)
45 | ruby-progressbar (~> 1.7)
46 | unicode-display_width (>= 2.4.0, < 3.0)
47 | rubocop-ast (1.31.3)
48 | parser (>= 3.3.1.0)
49 | rubocop-capybara (2.20.0)
50 | rubocop (~> 1.41)
51 | rubocop-discourse (3.8.0)
52 | activesupport (>= 6.1)
53 | rubocop (>= 1.59.0)
54 | rubocop-capybara (>= 2.0.0)
55 | rubocop-factory_bot (>= 2.0.0)
56 | rubocop-rails (>= 2.25.0)
57 | rubocop-rspec (>= 2.25.0)
58 | rubocop-factory_bot (2.25.1)
59 | rubocop (~> 1.41)
60 | rubocop-rails (2.25.0)
61 | activesupport (>= 4.2.0)
62 | rack (>= 1.1)
63 | rubocop (>= 1.33.0, < 2.0)
64 | rubocop-ast (>= 1.31.1, < 2.0)
65 | rubocop-rspec (2.29.2)
66 | rubocop (~> 1.40)
67 | rubocop-capybara (~> 2.17)
68 | rubocop-factory_bot (~> 2.22)
69 | rubocop-rspec_rails (~> 2.28)
70 | rubocop-rspec_rails (2.28.3)
71 | rubocop (~> 1.40)
72 | ruby-progressbar (1.13.0)
73 | syntax_tree (6.2.0)
74 | prettier_print (>= 1.2.0)
75 | tzinfo (2.0.6)
76 | concurrent-ruby (~> 1.0)
77 | unicode-display_width (2.5.0)
78 |
79 | PLATFORMS
80 | ruby
81 |
82 | DEPENDENCIES
83 | rubocop-discourse
84 | syntax_tree
85 |
86 | BUNDLED WITH
87 | 2.5.10
88 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018-2019 Civilized Discourse Construction Kit, Inc.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | 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, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # discourse-encrypt
2 |
3 | Discourse Encrypt is a plugin that provides a secure communication channel
4 | through Discourse. [Read more about the plugin on Meta...](https://meta.discourse.org/t/discourse-encrypt-for-private-messages/107918)
5 |
6 | ## Installation
7 |
8 | Follow [Install a Plugin](https://meta.discourse.org/t/install-a-plugin/19157)
9 | how-to from the official Discourse Meta, using `git clone https://github.com/discourse/discourse-encrypt.git`
10 | as the plugin command.
11 |
12 | Please note that WebCrypto API is restricted to secure origins, which basically
13 | means that you must enable HTTPS before using this plugin.
14 |
--------------------------------------------------------------------------------
/app/controllers/encrypted_post_timers_controller.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class DiscourseEncrypt::EncryptedPostTimersController < ApplicationController
4 | requires_plugin DiscourseEncrypt::PLUGIN_NAME
5 |
6 | before_action :ensure_logged_in
7 | before_action :ensure_can_encrypt
8 |
9 | def create
10 | delete_at = 1.minutes.from_now
11 | Array.wrap(params[:post_id]).each { |post_id| create_for_post(post_id, delete_at) }
12 | render json: { delete_at: delete_at }
13 | end
14 |
15 | def destroy
16 | post = Post.with_deleted.find(params[:post_id])
17 | encrypted_post_timer = EncryptedPostTimer.find_by(post: post)
18 | return unless encrypted_post_timer
19 | if post.is_first_post?
20 | topic = Topic.with_deleted.find(post.topic_id)
21 | guardian.ensure_can_recover_topic!(topic)
22 | else
23 | guardian.ensure_can_recover_post!(post)
24 | end
25 | encrypted_post_timer.destroy!
26 | end
27 |
28 | private
29 |
30 | def create_for_post(post_id, delete_at)
31 | post = Post.find(post_id)
32 | return unless post.is_encrypted?
33 | guardian.ensure_can_delete!(post.is_first_post? ? post.topic : post)
34 |
35 | EncryptedPostTimer.create!(post: post, delete_at: delete_at)
36 | end
37 |
38 | def ensure_can_encrypt
39 | current_user.guardian.ensure_can_encrypt!
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/app/jobs/scheduled/encrypt_consistency.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Jobs
4 | class EncryptConsistency < ::Jobs::Scheduled
5 | every 1.day
6 |
7 | def execute(args)
8 | DB
9 | .query(<<~SQL)
10 | SELECT taf.user_id, taf.topic_id
11 | FROM topic_allowed_users taf
12 | JOIN encrypted_topics_data etd ON taf.topic_id = etd.topic_id
13 | WHERE taf.user_id NOT IN
14 | (SELECT user_id
15 | FROM encrypted_topics_users
16 | WHERE topic_id = taf.topic_id)
17 | SQL
18 | .each do |row|
19 | Discourse.warn(
20 | "User was invited to encrypted topic, but has no topic key.",
21 | user_id: row.user_id,
22 | topic_id: row.topic_id,
23 | )
24 | TopicAllowedUser.find_by(user_id: row.user_id, topic_id: row.topic_id).delete
25 | end
26 |
27 | DB
28 | .query(<<~SQL)
29 | SELECT etu.user_id, etu.topic_id
30 | FROM encrypted_topics_users etu
31 | LEFT JOIN topic_allowed_users taf ON etu.topic_id = taf.topic_id AND etu.user_id = taf.user_id
32 | WHERE taf.id IS NULL
33 | SQL
34 | .each do |row|
35 | Discourse.warn(
36 | "User has topic key, but was not invited to topic.",
37 | user_id: row.user_id,
38 | topic_id: row.topic_id,
39 | )
40 | TopicAllowedUser.create(user_id: row.user_id, topic_id: row.topic_id)
41 | end
42 | end
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/app/jobs/scheduled/encrypted_post_timer_evaluator.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Jobs
4 | class EncryptedPostTimerEvaluator < ::Jobs::Scheduled
5 | every 1.minute
6 |
7 | def execute(args)
8 | EncryptedPostTimer.pending.find_each do |encrypted_post_timer|
9 | ActiveRecord::Base.transaction do
10 | encrypted_post_timer.touch(:destroyed_at)
11 |
12 | timer_post = Post.with_deleted.find_by(id: encrypted_post_timer.post_id)
13 | next if !timer_post
14 |
15 | timer_topic = Topic.with_deleted.find_by(id: timer_post.topic_id)
16 | next if !timer_topic
17 |
18 | posts_to_delete = find_posts_to_delete(timer_topic, timer_post)
19 | next if posts_to_delete.blank?
20 |
21 | timer_topic.update_columns(deleted_at: nil)
22 |
23 | posts_to_delete.each do |post|
24 | next if !post&.persisted?
25 | PostDestroyer.new(
26 | post.user || Discourse.system_user,
27 | post,
28 | permanent: true,
29 | force_destroy: true,
30 | ).destroy
31 | end
32 | end
33 | end
34 | end
35 |
36 | private
37 |
38 | def find_posts_to_delete(topic, post)
39 | (post.is_first_post? ? topic.posts.with_deleted.order(created_at: :desc) : [post]).compact
40 | end
41 | end
42 | end
43 |
--------------------------------------------------------------------------------
/app/mailers/user_notifications_extensions.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module DiscourseEncrypt::UserNotificationsExtensions
4 | def notification_email(user, opts)
5 | if opts[:post] && opts[:post].is_encrypted?
6 | opts[:allow_reply_by_email] = false
7 | opts[:notification_data_hash][
8 | :topic_title
9 | ] = "#{opts[:post].topic.title} ##{opts[:post].topic.id}"
10 | end
11 | super
12 | end
13 |
14 | module ClassMethods
15 | def get_context_posts(post, topic_user, user)
16 | return [] if post.is_encrypted?
17 | super
18 | end
19 | end
20 |
21 | def self.prepended(mod)
22 | mod.singleton_class.prepend(ClassMethods)
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/app/models/encrypted_post_timer.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class EncryptedPostTimer < ActiveRecord::Base
4 | belongs_to :post
5 |
6 | validates :post_id, presence: true
7 | validates :delete_at, presence: true
8 |
9 | scope :pending, -> { where(destroyed_at: nil).where("delete_at < ?", Time.zone.now) }
10 | end
11 |
12 | # == Schema Information
13 | #
14 | # Table name: encrypted_post_timers
15 | #
16 | # id :bigint not null, primary key
17 | # post_id :integer not null
18 | # delete_at :datetime not null
19 | # destroyed_at :datetime
20 | # created_at :datetime not null
21 | # updated_at :datetime not null
22 | #
23 | # Indexes
24 | #
25 | # index_encrypted_post_timers_on_delete_at (delete_at)
26 | # index_encrypted_post_timers_on_destroyed_at (destroyed_at)
27 | # index_encrypted_post_timers_on_post_id (post_id)
28 | #
29 |
--------------------------------------------------------------------------------
/app/models/encrypted_topics_data.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class EncryptedTopicsData < ActiveRecord::Base
4 | belongs_to :topic
5 | end
6 |
7 | # == Schema Information
8 | #
9 | # Table name: encrypted_topics_data
10 | #
11 | # id :bigint not null, primary key
12 | # topic_id :integer
13 | # title :text
14 | # created_at :datetime not null
15 | # updated_at :datetime not null
16 | #
17 | # Indexes
18 | #
19 | # index_encrypted_topics_data_on_topic_id (topic_id)
20 | #
21 |
--------------------------------------------------------------------------------
/app/models/encrypted_topics_user.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class EncryptedTopicsUser < ActiveRecord::Base
4 | belongs_to :topic
5 | belongs_to :user
6 | end
7 |
8 | # == Schema Information
9 | #
10 | # Table name: encrypted_topics_users
11 | #
12 | # id :bigint not null, primary key
13 | # user_id :integer
14 | # topic_id :integer
15 | # key :text
16 | #
17 | # Indexes
18 | #
19 | # index_encrypted_topics_users_on_topic_id (topic_id)
20 | # index_encrypted_topics_users_on_user_id (user_id)
21 | # index_encrypted_topics_users_on_user_id_and_topic_id (user_id,topic_id) UNIQUE
22 | #
23 |
--------------------------------------------------------------------------------
/app/models/user_encryption_key.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class UserEncryptionKey < ActiveRecord::Base
4 | belongs_to :user
5 | end
6 |
7 | # == Schema Information
8 | #
9 | # Table name: user_encryption_keys
10 | #
11 | # id :bigint not null, primary key
12 | # user_id :integer
13 | # encrypt_public :text
14 | # encrypt_private :text
15 | # created_at :datetime not null
16 | # updated_at :datetime not null
17 | #
18 | # Indexes
19 | #
20 | # index_user_encryption_keys_on_user_id (user_id)
21 | #
22 |
--------------------------------------------------------------------------------
/app/services/problem_check/unsafe_csp.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | class ProblemCheck::UnsafeCsp < ProblemCheck
4 | self.priority = "low"
5 |
6 | def call
7 | return no_problem if !SiteSetting.encrypt_enabled?
8 | return no_problem if !SiteSetting.content_security_policy?
9 | return no_problem if safe_policy?
10 |
11 | problem
12 | end
13 |
14 | private
15 |
16 | def safe_policy?
17 | DiscourseEncrypt.safe_csp_src?(SiteSetting.content_security_policy_script_src)
18 | end
19 |
20 | def translation_key
21 | "site_settings.errors.encrypt_unsafe_csp"
22 | end
23 | end
24 |
--------------------------------------------------------------------------------
/assets/javascripts/discourse/api-initializers/encrypt-icons.gjs:
--------------------------------------------------------------------------------
1 | import { computed } from "@ember/object";
2 | import { apiInitializer } from "discourse/lib/api";
3 | import icon from "discourse-common/helpers/d-icon";
4 | import i18n from "discourse-common/helpers/i18n";
5 | import { withSilencedDeprecations } from "discourse-common/lib/deprecated";
6 | import I18n from "discourse-i18n";
7 |
8 | export default apiInitializer("2.0.0", (api) => {
9 | withSilencedDeprecations("discourse.hbr-topic-list-overrides", () => {
10 | let topicStatusIcons;
11 | try {
12 | topicStatusIcons =
13 | require("discourse/helpers/topic-status-icons").default;
14 | } catch {}
15 |
16 | if (
17 | topicStatusIcons &&
18 | !topicStatusIcons.entries.find(
19 | ({ attribute }) => attribute === "encrypted_title"
20 | )
21 | ) {
22 | topicStatusIcons?.addObject([
23 | "encrypted_title",
24 | "user-secret",
25 | "encrypted",
26 | ]);
27 | }
28 |
29 | // topic-list-item icon
30 | api.modifyClass("raw-view:topic-status", {
31 | pluginId: "encrypt",
32 |
33 | statuses: computed(function () {
34 | const results = this._super(...arguments);
35 |
36 | if (this.topic.encrypted_title) {
37 | results.push({
38 | openTag: "span",
39 | closeTag: "span",
40 | title: I18n.t("topic-statuses.encrypted.help"),
41 | icon: "user-secret",
42 | key: "encrypted",
43 | });
44 | }
45 |
46 | return results;
47 | }),
48 | });
49 | });
50 |
51 | // Main topic title
52 | api.renderInOutlet("after-topic-status",
53 | {{~#if @outletArgs.topic.encrypted_title~}}
54 | {{icon "user-secret"}}
58 | {{~/if~}}
59 | );
60 | });
61 |
--------------------------------------------------------------------------------
/assets/javascripts/discourse/api-initializers/encrypt-model-transformers.js:
--------------------------------------------------------------------------------
1 | import { apiInitializer } from "discourse/lib/api";
2 | import { escapeExpression } from "discourse/lib/utilities";
3 | import {
4 | ENCRYPT_ACTIVE,
5 | getEncryptionStatus,
6 | getTopicTitle,
7 | putTopicKey,
8 | putTopicTitle,
9 | } from "discourse/plugins/discourse-encrypt/lib/discourse";
10 |
11 | export default apiInitializer("0.8", (api) => {
12 | const currentUser = api.getCurrentUser();
13 | if (getEncryptionStatus(currentUser) !== ENCRYPT_ACTIVE) {
14 | // No point adding these transforms if we can't actually decrypt
15 | return;
16 | }
17 |
18 | api.registerModelTransformer("topic", async (topics) => {
19 | for (const topic of topics) {
20 | if (topic.topic_key && topic.encrypted_title) {
21 | putTopicKey(topic.id, topic.topic_key);
22 | putTopicTitle(topic.id, topic.encrypted_title);
23 | try {
24 | const decryptedTitle = await getTopicTitle(topic.id);
25 | if (decryptedTitle) {
26 | topic.set("fancy_title", escapeExpression(decryptedTitle));
27 | topic.set("unicode_title", decryptedTitle);
28 | }
29 | } catch (err) {
30 | // eslint-disable-next-line no-console
31 | console.warn(
32 | `Decrypting the title of encrypted message (topicId: ${topic.id}) failed with the following error:`,
33 | err,
34 | err?.stack
35 | );
36 | }
37 | }
38 | }
39 | });
40 |
41 | api.registerModelTransformer("bookmark", async (bookmarks) => {
42 | for (const bookmark of bookmarks) {
43 | if (bookmark.topic_id && bookmark.topic_key && bookmark.encrypted_title) {
44 | putTopicKey(bookmark.topic_id, bookmark.topic_key);
45 | putTopicTitle(bookmark.topic_id, bookmark.encrypted_title);
46 | try {
47 | const decryptedTitle = await getTopicTitle(bookmark.topic_id);
48 | if (decryptedTitle) {
49 | bookmark.title = decryptedTitle;
50 | bookmark.fancy_title = escapeExpression(decryptedTitle);
51 | }
52 | } catch (err) {
53 | // eslint-disable-next-line no-console
54 | console.warn(
55 | `Decrypting the title of encrypted message (topicId: ${bookmark.topic_id}) failed with the following error:`,
56 | err,
57 | err?.stack
58 | );
59 | }
60 | }
61 | }
62 | });
63 |
64 | api.registerModelTransformer("notification", async (notifications) => {
65 | for (const notification of notifications) {
66 | if (
67 | notification.topic_id &&
68 | notification.topic_key &&
69 | notification.encrypted_title
70 | ) {
71 | putTopicKey(notification.topic_id, notification.topic_key);
72 | putTopicTitle(notification.topic_id, notification.encrypted_title);
73 | try {
74 | const decryptedTitle = await getTopicTitle(notification.topic_id);
75 | if (decryptedTitle) {
76 | notification.fancy_title = escapeExpression(decryptedTitle);
77 | }
78 | } catch (err) {
79 | // eslint-disable-next-line no-console
80 | console.warn(
81 | `Decrypting the title of encrypted message (topicId: ${notification.topic_id}) failed with the following error:`,
82 | err,
83 | err?.stack
84 | );
85 | }
86 | }
87 | }
88 | });
89 | });
90 |
--------------------------------------------------------------------------------
/assets/javascripts/discourse/components/encrypt-enable-dropdown.js:
--------------------------------------------------------------------------------
1 | import { computed } from "@ember/object";
2 | import { classNames } from "@ember-decorators/component";
3 | import I18n from "I18n";
4 | import DropdownSelectBoxComponent from "select-kit/components/dropdown-select-box";
5 | import { selectKitOptions } from "select-kit/components/select-kit";
6 |
7 | @selectKitOptions({
8 | icon: "bars",
9 | showFullTitle: false,
10 | })
11 | @classNames("encrypt-enable-dropdown")
12 | export default class EncryptEnableDropdown extends DropdownSelectBoxComponent {
13 | @computed("importIdentity", "isEncryptEnabled")
14 | get content() {
15 | const content = [];
16 |
17 | content.push({
18 | id: "import",
19 | icon: "file-import",
20 | name: this.importIdentity
21 | ? this.isEncryptEnabled
22 | ? I18n.t("encrypt.preferences.use_paper_key")
23 | : I18n.t("encrypt.preferences.generate_key")
24 | : I18n.t("encrypt.preferences.import"),
25 | });
26 |
27 | if (this.isEncryptEnabled) {
28 | content.push({
29 | id: "reset",
30 | icon: "trash-can",
31 | name: I18n.t("encrypt.reset.title"),
32 | });
33 | }
34 |
35 | return content;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/assets/javascripts/discourse/components/encrypt-preferences-dropdown.js:
--------------------------------------------------------------------------------
1 | import { classNames } from "@ember-decorators/component";
2 | import I18n from "I18n";
3 | import DropdownSelectBoxComponent from "select-kit/components/dropdown-select-box";
4 | import { selectKitOptions } from "select-kit/components/select-kit";
5 |
6 | @selectKitOptions({
7 | icon: "wrench",
8 | showFullTitle: false,
9 | })
10 | @classNames("encrypt-preferences-dropdown")
11 | export default class EncryptPreferencesDropdown extends DropdownSelectBoxComponent {
12 | content = [
13 | {
14 | id: "export",
15 | icon: "file-export",
16 | name: I18n.t("encrypt.export.title"),
17 | },
18 | {
19 | id: "managePaperKeys",
20 | icon: "ticket-simple",
21 | name: I18n.t("encrypt.manage_paper_keys.title"),
22 | },
23 | {
24 | id: "decryptAll",
25 | icon: "unlock",
26 | name: I18n.t("encrypt.decrypt_all.button"),
27 | },
28 | {
29 | id: "rotate",
30 | icon: "arrows-rotate",
31 | name: I18n.t("encrypt.rotate.title"),
32 | },
33 | {
34 | id: "reset",
35 | icon: "trash-can",
36 | name: I18n.t("encrypt.reset.title"),
37 | },
38 | ];
39 | }
40 |
--------------------------------------------------------------------------------
/assets/javascripts/discourse/components/encrypted-post-timer-dropdown.js:
--------------------------------------------------------------------------------
1 | import { computed } from "@ember/object";
2 | import { empty } from "@ember/object/computed";
3 | import { classNameBindings, classNames } from "@ember-decorators/component";
4 | import I18n from "I18n";
5 | import DropdownSelectBoxComponent from "select-kit/components/dropdown-select-box";
6 | import { selectKitOptions } from "select-kit/components/select-kit";
7 |
8 | const TIMER_OPTIONS = [
9 | { id: "", name: I18n.t("encrypt.time_bomb.never") },
10 | { id: "3", name: I18n.t("encrypt.time_bomb.3_minutes") },
11 | { id: "60", name: I18n.t("encrypt.time_bomb.1_hour") },
12 | { id: "180", name: I18n.t("encrypt.time_bomb.3_hours") },
13 | { id: "720", name: I18n.t("encrypt.time_bomb.12_hours") },
14 | { id: "1440", name: I18n.t("encrypt.time_bomb.24_hours") },
15 | { id: "4320", name: I18n.t("encrypt.time_bomb.3_days") },
16 | { id: "10080", name: I18n.t("encrypt.time_bomb.7_days") },
17 | ];
18 |
19 | @selectKitOptions({
20 | icon: "discourse-trash-clock",
21 | showFullTitle: true,
22 | })
23 | @classNames("encrypted-post-timer-dropdown")
24 | @classNameBindings("hidden:hidden")
25 | export default class EncryptedPostTimerDropdown extends DropdownSelectBoxComponent {
26 | @empty("content") hidden;
27 |
28 | @computed("topicDeleteAt")
29 | get content() {
30 | if (this.topicDeleteAt) {
31 | return TIMER_OPTIONS.filter((option) => {
32 | return (
33 | option.id.length > 0 &&
34 | moment().add(option.id, "minutes") < moment(this.topicDeleteAt)
35 | );
36 | });
37 | } else {
38 | return TIMER_OPTIONS;
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/assets/javascripts/discourse/components/modal/activate-encrypt.hbs:
--------------------------------------------------------------------------------
1 | {{i18n "encrypt.preferences.status_enabled_but_inactive"}}
14 |
17 |
25 | {{i18n "encrypt.export.instructions"}}
15 | {{i18n
16 | (if
17 | @model.device
18 | "encrypt.generate_paper_key.instructions_device"
19 | "encrypt.generate_paper_key.instructions"
20 | )
21 | }}
22 | {{this.paperKey}} {{i18n "encrypt.manage_paper_keys.instructions"}} {{i18n "encrypt.manage_paper_keys.no_key"}} {{i18n "encrypt.reset.instructions"}}
11 | {{html-safe
12 | (i18n
13 | "encrypt.reset.instructions_lost_pms" count=this.encryptedPmsCount
14 | )
15 | }}
16 | {{html-safe
17 | (i18n
18 | "encrypt.reset.confirm_instructions"
19 | username=this.currentUser.username
20 | )
21 | }}
22 | {{i18n "encrypt.reset.instructions_safe"}} {{i18n "encrypt.rotate.instructions"}}
10 | {{html-safe
11 | (i18n
12 | "encrypt.reset.confirm_instructions"
13 | username=this.currentUser.username
14 | )
15 | }}
16 | {{this.exported}}
9 |
10 |
11 | <:footer>
12 |
10 |
11 | {{#each this.keys as |key|}}
12 |
39 | {{else}}
40 |
13 |
36 | {{/each}}
37 |
38 |
14 | {{d-icon "key"}}
15 |
16 | {{#if key.isPaper}}
17 | {{key.name}}
18 | ...
19 | {{else if key.isPassphrase}}
20 |
21 | {{i18n "encrypt.manage_paper_keys.passphrase"}}
22 | {{i18n "encrypt.manage_paper_keys.not_recommended"}}
23 |
24 | {{/if}}
25 |
26 |
27 |
28 |
35 |
${prevRaw} | 52 |${currRaw} | 53 |
Будь ласка, вставте ключ шифрування, який ви експортували раніше.
42 | save: "Зберегти зміни" 43 | generate_paper_key: 44 | instructions: "Ось ваш унікальний паперовий ключ, він може дозволити вам активувати майбутні зашифровані пристрої. Це єдиний раз, коли ви це побачите, тому обов'язково запишіть його та зберігайте десь у безпеці." 45 | manage_paper_keys: 46 | instructions: "Нижче ви можете видалити будь-який зі створених раніше паперових ключів. Після видалення ключ неможливо відновити, тому переконайтеся, що у вас є хоча б один паперовий ключ або інші активні пристрої." 47 | delete: "Видалити" 48 | no_key: "У вас немає паперових ключів. Переконайтеся, що у вас є принаймні один ключ на папері або інші активні пристрої." 49 | passphrase: "на основі парольної фрази" 50 | not_recommended: "(не рекомендовано)" 51 | export: 52 | instructions: "Експорт вашої пари ключів - ще один механізм резервного копіювання. Зауважте, що експортована пара ключів незахищена, і вона зможе розшифрувати всі ваші приватні розмови." 53 | copy_to_clipboard: "Скопіювати в буфер" 54 | copied_to_clipboard: "Скопійовано у буфер обміну" 55 | rotate: 56 | loading_states: 57 | finished: "Виконано!" 58 | time_bomb: 59 | never: "Ніколи" 60 | 3_minutes: "3 хвилин" 61 | 1_hour: "1 година" 62 | 3_hours: "3 годин" 63 | 12_hours: "12 годин" 64 | 24_hours: "24 години" 65 | 3_days: "3 днів" 66 | 7_days: "7 днів" 67 | topic-statuses: 68 | encrypted: 69 | help: "Це повідомлення зашифроване." 70 | -------------------------------------------------------------------------------- /config/locales/client.ur.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | ur: 8 | js: 9 | encrypt: 10 | decrypt_permanently: 11 | refresh_page: "رِیفریش" 12 | decrypt_all: 13 | done: "مکمل" 14 | preferences: 15 | save: "تبدیلیاں محفوظ کریں" 16 | manage_paper_keys: 17 | delete: "حذف کریں" 18 | export: 19 | copy_to_clipboard: "کلِپ بورڈ میں کاپی کریں" 20 | copied_to_clipboard: "کلِپ بورڈ میں کاپی کر لیا گیا" 21 | rotate: 22 | loading_states: 23 | finished: "مکمل!" 24 | time_bomb: 25 | never: "کبھی نہیں" 26 | 3_minutes: "3 منٹ" 27 | 1_hour: "1 گھنٹہ" 28 | 3_hours: "3 گھنٹے" 29 | 12_hours: "12 گھنٹے" 30 | 24_hours: "24 گھنٹے" 31 | -------------------------------------------------------------------------------- /config/locales/client.vi.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | vi: 8 | js: 9 | encrypt: 10 | decrypt_permanently: 11 | refresh_page: "Làm mới" 12 | decrypt_all: 13 | done: "Hoàn tất" 14 | post: 15 | delete: 16 | title: "Bạn có chắc không?" 17 | preferences: 18 | save: "Lưu thay đổi" 19 | manage_paper_keys: 20 | delete: "Xóa" 21 | export: 22 | copy_to_clipboard: "Sao chép vào clipboard" 23 | copied_to_clipboard: "Sao chép vào Clipboard" 24 | rotate: 25 | loading_states: 26 | finished: "Hoàn tất!" 27 | time_bomb: 28 | never: "Không bao giờ" 29 | 3_minutes: "3 phút" 30 | 1_hour: "1 tiếng" 31 | 3_hours: "3 tiếng" 32 | 12_hours: "12 tiếng" 33 | 24_hours: "24 tiếng" 34 | 3_days: "3 ngày" 35 | 7_days: "7 ngày" 36 | -------------------------------------------------------------------------------- /config/locales/client.zh_TW.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | zh_TW: 8 | js: 9 | encrypt: 10 | decrypt_permanently: 11 | refresh_page: "重新整理" 12 | decrypt_all: 13 | done: "完成" 14 | preferences: 15 | save: "儲存變更" 16 | manage_paper_keys: 17 | delete: "刪除" 18 | export: 19 | copy_to_clipboard: "複製至剪貼簿" 20 | copied_to_clipboard: "已複製至剪貼簿" 21 | rotate: 22 | loading_states: 23 | finished: "完成!" 24 | time_bomb: 25 | never: "永不" 26 | 3_minutes: "3 分鐘" 27 | 1_hour: "1 小時" 28 | 3_hours: "3 小時" 29 | 12_hours: "12 小時" 30 | 24_hours: "24 小時" 31 | -------------------------------------------------------------------------------- /config/locales/server.ar.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | ar: 8 | encrypt: 9 | encrypted_excerpt: "هذه الرسالة مشفرة، يُرجى قراءتها في المتصفح." 10 | enabled_already: "لقد فعَّلت الرسائل المشفرة بالفعل." 11 | only_pms: "يمكن تشفير الرسائل الخاصة فقط." 12 | no_encrypt_keys: "حدث خطأ ما. لم يتم تضمين أي مفاتيح تشفير في الحمولة." 13 | site_settings: 14 | encrypt_enabled: "تفعيل الرسائل الخاصة المشفرة" 15 | auto_enable_encrypt: "تمكين التشفير تلقائيًا لجميع المستخدمين الذين سجَّلوا الدخول." 16 | encrypt_groups: "اسم المجموعات التي يمكنها استخدام التشفير (ترك الاسم فارغًا يعني أن الجميع يمكنهم استخدام التشفير)" 17 | encrypt_pms_default: "تشفير جميع الرسائل الخاصة الجديدة افتراضيًا" 18 | allow_new_encrypted_pms: "السماح للمستخدمين بإنشاء رسائل خاصة مشفرة جديدة. سيؤدي تعطيل هذا الإعداد إلى تقييد استخدام discourse-encrypt إلى الرسائل الخاصة الحالية." 19 | allow_decrypting_pms: "السماح للمستخدمين بفك تشفير الرسائل الخاصة المشفرة بشكلٍ دائم" 20 | errors: 21 | encrypt_unsafe_csp: "لا يمكن استخدام توجيهات CSP غير الآمنة مثل 'unsafe-eval' و'unsafe-inline' عند تفعيل المكوِّن الإضافي للتشفير في Discourse." 22 | dashboard: 23 | problem: 24 | encrypt_deprecated: تم إيقاف استخدام المكوِّن الإضافي discourse-encrypt، وسيتم إيقاف الدعم في الربع الأول من عام 2025. للمزيد من المعلومات، راجع Discourse Meta. قم بإلغاء تثبيت المكوِّن الإضافي لإزالة هذه الرسالة. 25 | -------------------------------------------------------------------------------- /config/locales/server.be.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | be: 8 | -------------------------------------------------------------------------------- /config/locales/server.bg.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | bg: 8 | -------------------------------------------------------------------------------- /config/locales/server.bs_BA.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | bs_BA: 8 | -------------------------------------------------------------------------------- /config/locales/server.ca.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | ca: 8 | -------------------------------------------------------------------------------- /config/locales/server.cs.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | cs: 8 | -------------------------------------------------------------------------------- /config/locales/server.da.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | da: 8 | -------------------------------------------------------------------------------- /config/locales/server.de.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | de: 8 | encrypt: 9 | encrypted_excerpt: "Diese Nachricht ist verschlüsselt, lies sie bitte im Browser." 10 | enabled_already: "Du hast bereits verschlüsselte Nachrichten aktiviert." 11 | only_pms: "Nur private Nachrichten können verschlüsselt werden." 12 | no_encrypt_keys: "Etwas ist schiefgelaufen. Es sind keine Verschlüsselungsschlüssel im Payload enthalten." 13 | site_settings: 14 | encrypt_enabled: "Aktiviere verschlüsselte private Nachrichten." 15 | auto_enable_encrypt: "Verschlüsselung für alle angemeldeten Benutzer automatisch aktivieren." 16 | encrypt_groups: "Der Name der Gruppen, die die Verschlüsselung nutzen können (leer bedeutet alle)." 17 | encrypt_pms_default: "Neue private Nachrichten standardmäßig verschlüsseln." 18 | allow_new_encrypted_pms: "Erlaube Benutzern, neue verschlüsselte private Nachrichten zu erstellen. Wenn du diese Option deaktivierst, wird die Nutzung von discourse-encrypt auf bestehende verschlüsselte PN beschränkt." 19 | allow_decrypting_pms: "Erlaube Benutzern, verschlüsselte private Nachrichten dauerhaft zu entschlüsseln." 20 | errors: 21 | encrypt_unsafe_csp: "Unsichere CSP-Direktiven wie „unsafe-eval“ und „unsafe-inline“ können nicht verwendet werden, wenn das Discourse-Encrypt-Plug-in aktiviert ist." 22 | dashboard: 23 | problem: 24 | encrypt_deprecated: Das Plug-in „discourse-encrypt“ ist veraltet und wird ab dem 1. Quartal 2025 nicht mehr unterstützt. Weitere Informationen findest du unter Discourse Meta. Deinstalliere das Plug-in, um diese Meldung zu entfernen. 25 | -------------------------------------------------------------------------------- /config/locales/server.el.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | el: 8 | -------------------------------------------------------------------------------- /config/locales/server.en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | encrypt: 3 | encrypted_excerpt: "This message is encrypted, please read it in the browser." 4 | enabled_already: "You have already enabled encrypted messages." 5 | only_pms: "Only private messages can be encrypted." 6 | no_encrypt_keys: "Something went wrong. No encryption keys were included in the payload." 7 | 8 | site_settings: 9 | encrypt_enabled: "Enable encrypted private messages." 10 | auto_enable_encrypt: "Automatically enable encrypt for all logged in users." 11 | encrypt_groups: "The name of groups that are able to use encryption (empty means everyone)." 12 | encrypt_pms_default: "Encrypt all new private messages by default." 13 | allow_new_encrypted_pms: "Allow users to create new encrypted private messages. Disabling this will limit discourse-encrypt use to existing encrypted PMs." 14 | allow_decrypting_pms: "Allow users to permanently decrypt encrypted private messages." 15 | 16 | errors: 17 | encrypt_unsafe_csp: "Unsafe CSP directives like 'unsafe-eval' and 'unsafe-inline' cannot be used when the Discourse Encrypt plugin is enabled." 18 | 19 | dashboard: 20 | problem: 21 | encrypt_deprecated: The discourse-encrypt plugin is deprecated, and support will be dropped in Q1 2025. For more information, see Discourse Meta. Uninstall the plugin to remove this message. -------------------------------------------------------------------------------- /config/locales/server.en_GB.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | en_GB: 8 | -------------------------------------------------------------------------------- /config/locales/server.es.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | es: 8 | encrypt: 9 | encrypted_excerpt: "Este mensaje está cifrado, léelo en el navegador." 10 | enabled_already: "Ya has habilitado los mensajes cifrados." 11 | only_pms: "Solo se pueden cifrar los mensajes privados." 12 | no_encrypt_keys: "Algo salió mal. No se incluyeron claves de cifrado en la carga útil." 13 | site_settings: 14 | encrypt_enabled: "Habilitar los mensajes privados cifrados." 15 | auto_enable_encrypt: "Habilitar automáticamente el cifrado para todos los usuarios que hayan iniciado sesión." 16 | encrypt_groups: "El nombre de los grupos que pueden usar cifrado (vacío significa todos)." 17 | encrypt_pms_default: "Cifrar todos los mensajes privados nuevos por defecto." 18 | allow_new_encrypted_pms: "Permitir a los usuarios crear nuevos mensajes privados cifrados. Desactivar esta opción limitará el uso del cifrado de Discourse a los MP cifrados existentes." 19 | allow_decrypting_pms: "Permitir a los usuarios descifrar permanentemente los mensajes privados cifrados." 20 | errors: 21 | encrypt_unsafe_csp: "Las directivas CSP inseguras, como «unsafe-eval» y «unsafe-inline», no pueden utilizarse cuando el complemento Discourse Encrypt está activado." 22 | dashboard: 23 | problem: 24 | encrypt_deprecated: El complemento discourse-encrypt está obsoleto y dejará de ser compatible en el primer trimestre de 2025. Para más información, consulta Discourse Meta. Desinstala el plugin para eliminar este mensaje. 25 | -------------------------------------------------------------------------------- /config/locales/server.et.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | et: 8 | -------------------------------------------------------------------------------- /config/locales/server.fa_IR.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | fa_IR: 8 | site_settings: 9 | auto_enable_encrypt: "به طور خودکار رمزگذاری را برای همه کاربران وارد شده در سیستم فعال کنید." 10 | encrypt_pms_default: "تمام پیامهای خصوصی جدید را به طور پیشفرض رمزگذاری کنید." 11 | -------------------------------------------------------------------------------- /config/locales/server.fi.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | fi: 8 | encrypt: 9 | encrypted_excerpt: "Tämä viesti on salattu, lue se selaimessa." 10 | enabled_already: "Olet jo ottanut salatut viestit käyttöön." 11 | only_pms: "Vain yksityisiä viestejä voi salata." 12 | no_encrypt_keys: "Jokin meni vikaan. Salausavaimia ei sisällytetty tietoihin." 13 | site_settings: 14 | encrypt_enabled: "Ota salatut yksityisviestit käyttöön." 15 | auto_enable_encrypt: "Ota salaus käyttöön automaattisesti kaikille kirjautuneille käyttäjille." 16 | encrypt_groups: "Niiden ryhmien nimet, jotka voivat käyttää salausta (tyhjä tarkoittaa kaikkia)." 17 | encrypt_pms_default: "Salaa kaikki uudet yksityisviestit oletuksena." 18 | allow_new_encrypted_pms: "Salli käyttäjien luoda uusia salattuja yksityisviestejä. Tämän poistaminen käytöstä rajoittaa discourse-encryptin käytön olemassa oleviin salattuihin yksityisviesteihin." 19 | allow_decrypting_pms: "Salli käyttäjien purkaa salattujen yksityisviestien salaus pysyvästi." 20 | errors: 21 | encrypt_unsafe_csp: "Turvattomia CSP-komentoja, kuten \"unsafe-eval\" ja \"unsafe-inline\", ei voida käyttää, kun Discoursen salauslisäosa on käytössä." 22 | dashboard: 23 | problem: 24 | encrypt_deprecated: Discourse-encrypt-lisäosa on vanhentunut, ja sen tuki lopetetaan vuoden 2025 ensimmäisellä neljänneksellä. Lisätietoja on Discourse Metassa. Poista lisäosa poistaaksesi tämän viestin. 25 | -------------------------------------------------------------------------------- /config/locales/server.fr.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | fr: 8 | encrypt: 9 | encrypted_excerpt: "Ce message est chiffré, veuillez le lire dans le navigateur." 10 | enabled_already: "Vous avez déjà activé les messages chiffrés." 11 | only_pms: "Seuls les messages privés peuvent être chiffrés." 12 | no_encrypt_keys: "Une erreur s'est produite. Aucune clé de chiffrement n'a été incluse dans les données reçues." 13 | site_settings: 14 | encrypt_enabled: "Activer les messages privés chiffrés." 15 | auto_enable_encrypt: "Activez automatiquement le chiffrement pour tous les utilisateurs connectés." 16 | encrypt_groups: "Le nom des groupes qui peuvent utiliser le chiffrement (laissez l'espace vide pour accorder cette autorisation à tout le monde)." 17 | encrypt_pms_default: "Chiffrez par défaut tous les nouveaux messages privés." 18 | allow_new_encrypted_pms: "Autoriser les utilisateurs à créer de nouveaux messages privés chiffrés. La désactivation de cette option limitera l'utilisation de discourse-encrypt aux MD chiffrés existants." 19 | allow_decrypting_pms: "Permettre aux utilisateurs de déchiffrer de façon permanente les messages privés chiffrés." 20 | errors: 21 | encrypt_unsafe_csp: "Les directives CSP non sécurisées telles que « unsafe-eval » et « unsafe-inline » ne peuvent pas être utilisées lorsque l'extension Discourse Encrypt est activée." 22 | dashboard: 23 | problem: 24 | encrypt_deprecated: Le plugin discourse-encrypt est obsolète et sa prise en charge sera abandonnée au premier trimestre 2025. Pour en savoir plus, consultez Discourse Meta. Désinstallez l'extension pour supprimer ce message. 25 | -------------------------------------------------------------------------------- /config/locales/server.gl.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | gl: 8 | -------------------------------------------------------------------------------- /config/locales/server.he.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | he: 8 | encrypt: 9 | encrypted_excerpt: "הודעה זו מוצפנת. נא לקרוא אותה בדפדפן." 10 | enabled_already: "כבר הפעלת הודעות מוצפנות." 11 | only_pms: "ניתן להצפין הודעות פרטיות בלבד." 12 | no_encrypt_keys: "משהו השתבש. לא נמצאו מפתחות הצפנה במטען." 13 | site_settings: 14 | encrypt_enabled: "הפעלת הודעות פרטיות מוצפנות." 15 | auto_enable_encrypt: "להפעיל הצפנה אוטומטית לכל המשתמשים שנכנסו למערכת." 16 | encrypt_groups: "שמות הקבוצות שיכולים להשתמש בהצפנה (ריק כלומר כולן יכולות)." 17 | encrypt_pms_default: "להצפין את כל ההודעות הפרטיות החדשות כבררת מחדל." 18 | allow_new_encrypted_pms: "לאפשר למשתמש ליצור הודעות מוצפנות חדשות. השבתת האפשרות הזאת תגביל את discourse-encrypt להשתמש בהודעות מוצפנות קיימות." 19 | allow_decrypting_pms: "לאפשר למשתמשים לפענח הודעות פרטיות מוצפנות לצמיתות." 20 | errors: 21 | encrypt_unsafe_csp: "אי אפשר להשתמש בהנחיות CSP מפוקפקות כגון ‚unsafe-eval’ ו־‚unsafe-inline’ כאשר התוסף Encrypt של Discourse פעיל." 22 | dashboard: 23 | problem: 24 | encrypt_deprecated: תוסף discourse-encrypt לא בפיתוח עוד והתמיכה תוסר ברבעון הראשון של 2025. למידע נוסף, נא לגשת אל Discourse Meta. הסרת התקנת התוסף תסיר את ההודעה הזאת. 25 | -------------------------------------------------------------------------------- /config/locales/server.hr.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | hr: 8 | -------------------------------------------------------------------------------- /config/locales/server.hu.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | hu: 8 | -------------------------------------------------------------------------------- /config/locales/server.hy.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | hy: 8 | -------------------------------------------------------------------------------- /config/locales/server.id.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | id: 8 | -------------------------------------------------------------------------------- /config/locales/server.it.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | it: 8 | encrypt: 9 | encrypted_excerpt: "Questo messaggio è crittografato, per favore leggilo nel browser." 10 | enabled_already: "Hai già abilitato i messaggi crittografati." 11 | only_pms: "Solo i messaggi privati possono essere crittografati." 12 | no_encrypt_keys: "Qualcosa è andato storto. Nessuna chiave di crittografia è stata inclusa nel payload." 13 | site_settings: 14 | encrypt_enabled: "Abilita messaggi privati crittografati." 15 | auto_enable_encrypt: "Abilita automaticamente la crittografia per tutti gli utenti che hanno effettuato l'accesso." 16 | encrypt_groups: "Nome dei gruppi che possono utilizzare la crittografia (vuoto significa tutti)." 17 | encrypt_pms_default: "Applica crittografia a tutti i nuovi messaggi privati per impostazione predefinita." 18 | allow_new_encrypted_pms: "Consenti agli utenti di creare nuovi messaggi privati crittografati. Disattivando questa opzione, si limiterà l'uso di discourse-encrypt ai MP crittografati esistenti." 19 | allow_decrypting_pms: "Consenti agli utenti di decifrare in modo permanente i messaggi privati crittografati." 20 | errors: 21 | encrypt_unsafe_csp: "Le direttive CSP non sicure come \"unsafe-eval\" e \"unsafe-inline\" non possono essere utilizzate quando il plug-in Discourse Encrypt è abilitato." 22 | dashboard: 23 | problem: 24 | encrypt_deprecated: Il plugin discourse-encrypt è deprecato e il supporto verrà interrotto nel primo trimestre del 2025. Per maggiori informazioni, consulta il Meta di Discourse. Disinstalla il plugin per far sparire questo messaggio. 25 | -------------------------------------------------------------------------------- /config/locales/server.ja.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | ja: 8 | encrypt: 9 | encrypted_excerpt: "このメッセージは暗号化されています。ブラウザでお読みください。" 10 | enabled_already: "暗号化メッセージはすでに有効になっています。" 11 | only_pms: "プライベートメッセージのみを暗号化できます。" 12 | no_encrypt_keys: "問題が発生しました。ペイロードに暗号化キーが含まれていません。" 13 | site_settings: 14 | encrypt_enabled: "暗号化プライベートメッセージを有効にします。" 15 | auto_enable_encrypt: "ログインしているすべてのユーザーに暗号化を自動的に有効にします。" 16 | encrypt_groups: "暗号化を使用できるグループの名前(全員の場合は空白にします)。" 17 | encrypt_pms_default: "デフォルトですべての新しいプライベートメッセージを暗号化します。" 18 | allow_new_encrypted_pms: "ユーザーが暗号化されたプライベートメッセージを新規に作成することを許可する。これを無効にすると、discourse-encrypt の使用が既存の暗号化された PM に制限されます。" 19 | allow_decrypting_pms: "ユーザーが暗号化されたプライベートメッセージを永久的に復号化することを許可する。" 20 | errors: 21 | encrypt_unsafe_csp: "Discourse Encrypt プラグインが有効である場合、「unsafe-eval」や「unsafe-inline」などの安全でない CSP ディレクティブを使用できません。" 22 | dashboard: 23 | problem: 24 | encrypt_deprecated: discourse-encrypt プラグインは使用廃止であり、2025 年第 1 四半期にサポート終了となる予定です。詳細については、Discourse Meta をご覧ください。このメッセージを削除するには、プラグインをアンインストールしてください。 25 | -------------------------------------------------------------------------------- /config/locales/server.ko.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | ko: 8 | -------------------------------------------------------------------------------- /config/locales/server.lt.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | lt: 8 | -------------------------------------------------------------------------------- /config/locales/server.lv.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | lv: 8 | -------------------------------------------------------------------------------- /config/locales/server.nb_NO.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | nb_NO: 8 | -------------------------------------------------------------------------------- /config/locales/server.nl.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | nl: 8 | encrypt: 9 | encrypted_excerpt: "Dit bericht is versleuteld; lees het in de browser." 10 | enabled_already: "Je hebt versleutelde berichten al ingeschakeld." 11 | only_pms: "Alleen privéberichten kunnen worden versleuteld." 12 | no_encrypt_keys: "Er is iets misgegaan. Er waren geen sleutels opgenomen in de payload." 13 | site_settings: 14 | encrypt_enabled: "Versleutelde privéberichten inschakelen." 15 | auto_enable_encrypt: "Automatisch versleuteling inschakelen voor alle aangemelde gebruikers." 16 | encrypt_groups: "De naam van groepen die versleuteling kunnen gebruiken (leeg betekent iedereen)." 17 | encrypt_pms_default: "Alle nieuwe privéberichten standaard versleutelen." 18 | allow_new_encrypted_pms: "Sta gebruikers toe om nieuwe versleutelde privéberichten te maken. Door dit uit te schakelen, wordt het gebruik van discourse-encrypt beperkt tot bestaande versleutelde PB's." 19 | allow_decrypting_pms: "Sta gebruikers toestaan om versleutelde privéberichten permanent te ontsleutelen." 20 | errors: 21 | encrypt_unsafe_csp: "Onveilige CSP-instructies zoals 'unsafe-eval' en 'unsafe-inline' kunnen niet worden gebruikt als de Discourse Encrypt-plug-in is ingeschakeld." 22 | dashboard: 23 | problem: 24 | encrypt_deprecated: De plug-in discourse-encrypt is verouderd en de ondersteuning wordt gestopt in het eerste kwartaal van 2025. Zie Discourse Meta voor meer informatie. Verwijder de plug-in om deze melding niet meer te zien. 25 | -------------------------------------------------------------------------------- /config/locales/server.pl_PL.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | pl_PL: 8 | encrypt: 9 | encrypted_excerpt: "Ta wiadomość jest zaszyfrowana, przeczytaj ją w przeglądarce." 10 | enabled_already: "Masz już włączone szyfrowane wiadomości." 11 | only_pms: "Można szyfrować tylko wiadomości prywatne." 12 | no_encrypt_keys: "Coś poszło nie tak. W zapytaniu nie uwzględniono kluczy szyfrowania." 13 | site_settings: 14 | encrypt_enabled: "Włącz zaszyfrowane wiadomości prywatne." 15 | auto_enable_encrypt: "Automatycznie włącz szyfrowanie dla wszystkich zalogowanych użytkowników." 16 | encrypt_groups: "Nazwy grup, które mogą używać szyfrowania (puste oznacza wszystkich)." 17 | encrypt_pms_default: "Domyślnie szyfruj wszystkie nowe wiadomości prywatne." 18 | errors: 19 | encrypt_unsafe_csp: "Niebezpieczne dyrektywy CSP, takie jak 'unsafe-eval' i 'unsafe-inline', nie mogą być używane, gdy wtyczka Discourse Encrypt jest włączona." 20 | -------------------------------------------------------------------------------- /config/locales/server.pt.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | pt: 8 | encrypt: 9 | encrypted_excerpt: "Esta mensagem está encriptada, por favor, leia-a no navegador." 10 | enabled_already: "Já ativou as mensagens encriptadas." 11 | only_pms: "Apenas as mensagens privadas podem ser encriptadas." 12 | no_encrypt_keys: "Ocorreu algo de errado. Não foram incluídas chaves de encriptação na carga útil." 13 | site_settings: 14 | encrypt_enabled: "Ative as mensagens privadas encriptadas." 15 | encrypt_groups: "O nome dos grupos que podem utilizar a encriptação (vazio significa toda a gente)." 16 | -------------------------------------------------------------------------------- /config/locales/server.pt_BR.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | pt_BR: 8 | encrypt: 9 | encrypted_excerpt: "Esta mensagem está criptografada, leia-a no navegador." 10 | enabled_already: "Você já ativou mensagens criptografadas." 11 | only_pms: "É possível criptografar apenas mensagens privadas" 12 | no_encrypt_keys: "Algo deu errado. Nenhuma chave de criptografia foi inclusa no conteúdo." 13 | site_settings: 14 | encrypt_enabled: "Ative mensagens privadas criptografadas." 15 | auto_enable_encrypt: "Ative criptografia automaticamente para todos(as) os(as) usuários(as) que entraram." 16 | encrypt_groups: "O nome dos grupos que podem usar criptografia (vazio quer dizer todas as pessoas)." 17 | encrypt_pms_default: "Criptografe todas as mensagens privadas por padrão." 18 | allow_new_encrypted_pms: "Permitir que usuários(as) criem novas mensagens privadas criptografadas. Desabilite essa opção para limitar o uso de criptografia do Discourse a todas as PM criptografadas." 19 | allow_decrypting_pms: "Permitir que usuários(as) descriptografem permanentemente mensagens privadas criptografadas." 20 | errors: 21 | encrypt_unsafe_csp: "Diretivas CSP não seguras, como \"unsafe-eval\" e \"unsafe-inline\", não podem ser usadas quando o plugin Discourse Encrypt estiver ativado." 22 | dashboard: 23 | problem: 24 | encrypt_deprecated: O plugin discourse-encrypt está obsoleto, e o suporte será descontinuado no primeiro trimestre de 2025. Para obter mais informações, consulte Discourse Meta. Desinstale o plugin para remover esta mensagem. 25 | -------------------------------------------------------------------------------- /config/locales/server.ro.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | ro: 8 | -------------------------------------------------------------------------------- /config/locales/server.ru.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | ru: 8 | encrypt: 9 | encrypted_excerpt: "Сообщение зашифровано, откройте его в браузере." 10 | enabled_already: "Вы уже включили шифрование сообщений." 11 | only_pms: "Зашифрованы могут быть только личные сообщения." 12 | no_encrypt_keys: "Что-то пошло не так. Ключи шифрования не были задействованы." 13 | site_settings: 14 | encrypt_enabled: "Включить шифрование личных сообщений." 15 | auto_enable_encrypt: "Автоматически включать шифрование для всех входящих в систему пользователей." 16 | encrypt_groups: "Названия групп, которые могут использовать шифрование (оставьте поле пустым, если необходимо включить функционал для всех групп)." 17 | encrypt_pms_default: "По умолчанию шифровать все новые личные сообщения." 18 | allow_new_encrypted_pms: "Разрешает пользователям создавать новые зашифрованные личные сообщения. Отключение этого параметра ограничит использование плагина discourse-encrypt существующими зашифрованными ЛС." 19 | allow_decrypting_pms: "Разрешает пользователям навсегда расшифровать зашифрованные личные сообщения." 20 | errors: 21 | encrypt_unsafe_csp: "Небезопасные директивы CSP, такие как 'unsafe-eval' и 'unsafe-inline', нельзя использовать с включенным плагином Discourse Encrypt." 22 | dashboard: 23 | problem: 24 | encrypt_deprecated: Плагин discourse-encrypt устарел, и его поддержка будет прекращена в первом квартале 2025 года. Для получения дополнительной информации см. Discourse Meta. Удалите плагин, чтобы убрать это сообщение. 25 | -------------------------------------------------------------------------------- /config/locales/server.sk.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | sk: 8 | -------------------------------------------------------------------------------- /config/locales/server.sl.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | sl: 8 | -------------------------------------------------------------------------------- /config/locales/server.sq.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | sq: 8 | -------------------------------------------------------------------------------- /config/locales/server.sr.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | sr: 8 | -------------------------------------------------------------------------------- /config/locales/server.sv.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | sv: 8 | encrypt: 9 | encrypted_excerpt: "Detta meddelande är krypterat, läs det i webbläsaren." 10 | enabled_already: "Du har redan aktiverat krypterade meddelanden." 11 | only_pms: "Enbart privata meddelanden kan krypteras." 12 | no_encrypt_keys: "Något gick fel. Inga krypteringsnycklar inkluderades i innehållet." 13 | site_settings: 14 | encrypt_enabled: "Aktivera krypterade privata meddelanden." 15 | auto_enable_encrypt: "Aktivera automatiskt kryptering för alla inloggade användare." 16 | encrypt_groups: "Namnen på grupper som ska vara tillåtna att använda kryptering (tomt innebär samtliga)" 17 | encrypt_pms_default: "Kryptera alla nya privata meddelanden som standard." 18 | errors: 19 | encrypt_unsafe_csp: "Osäkra CSP-direktiv som 'unsafe-eval' och 'unsafe-inline' kan inte användas när insticksprogrammet Discourse Encrypt är aktiverad." 20 | -------------------------------------------------------------------------------- /config/locales/server.sw.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | sw: 8 | -------------------------------------------------------------------------------- /config/locales/server.te.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | te: 8 | -------------------------------------------------------------------------------- /config/locales/server.th.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | th: 8 | -------------------------------------------------------------------------------- /config/locales/server.tr_TR.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | tr_TR: 8 | encrypt: 9 | encrypted_excerpt: "Bu mesaj şifreli, lütfen tarayıcıda okuyun." 10 | enabled_already: "Şifreli mesajları zaten etkinleştirdiniz." 11 | only_pms: "Yalnızca özel mesajlar şifrelenebilir." 12 | no_encrypt_keys: "Bir şeyler ters gitti. Yüke şifreleme anahtarı dahil edilmedi." 13 | site_settings: 14 | encrypt_enabled: "Şifrelenmiş özel mesajları etkinleştirin." 15 | auto_enable_encrypt: "Oturum açmış tüm kullanıcılar için şifrelemeyi otomatik olarak etkinleştirin." 16 | encrypt_groups: "Şifreleme kullanabilen grupların adı (boş, herkes anlamına gelir)." 17 | encrypt_pms_default: "Tüm yeni özel mesajları varsayılan olarak şifreleyin." 18 | allow_new_encrypted_pms: "Kullanıcıların yeni şifreli kişisel mesajlar oluşturmasına izin verin. Bunu devre dışı bırakmak discourse-encrypt kullanımını mevcut şifrelenmiş kişisel mesajlarla sınırlar." 19 | allow_decrypting_pms: "Kullanıcıların şifrelenmiş özel mesajların şifresini kalıcı olarak çözmesine izin verin." 20 | errors: 21 | encrypt_unsafe_csp: "Discourse Encrypt eklentisi etkinleştirildiğinde \"unsafe-eval\" ve \"unsafe-inline\" gibi güvenli olmayan CSP yönergeleri kullanılamaz." 22 | dashboard: 23 | problem: 24 | encrypt_deprecated: discourse-encrypt eklentisi kullanımdan kaldırıldı ve destek, 2025'in 1. çeyreğinde sona erecek. Daha fazla bilgi için Discourse Meta bölümüne bakın. Bu mesajı silmek için eklentiyi kaldırın. 25 | -------------------------------------------------------------------------------- /config/locales/server.ug.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | ug: 8 | -------------------------------------------------------------------------------- /config/locales/server.uk.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | uk: 8 | -------------------------------------------------------------------------------- /config/locales/server.ur.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | ur: 8 | -------------------------------------------------------------------------------- /config/locales/server.vi.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | vi: 8 | -------------------------------------------------------------------------------- /config/locales/server.zh_CN.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | zh_CN: 8 | encrypt: 9 | encrypted_excerpt: "此消息已加密,请在浏览器中阅读。" 10 | enabled_already: "您已启用加密消息。" 11 | only_pms: "只能加密私信。" 12 | no_encrypt_keys: "出了点问题。有效负载中未包含加密密钥。" 13 | site_settings: 14 | encrypt_enabled: "启用加密私信。" 15 | auto_enable_encrypt: "自动为所有登录用户启用加密。" 16 | encrypt_groups: "能够使用加密的群组的名称(留空表示所有人都可以使用)。" 17 | encrypt_pms_default: "默认情况下加密所有新的私信。" 18 | allow_new_encrypted_pms: "允许用户创建新的加密私信。禁用此功能会将 discourse-encrypt 的使用限定为现有加密私信。" 19 | allow_decrypting_pms: "允许用户永久解密加密私信。" 20 | errors: 21 | encrypt_unsafe_csp: "启用 Discourse Encrypt 插件后,将无法使用诸如 'unsafe-eval' 和 'unsafe-inline' 等不安全的 CSP 指令。" 22 | dashboard: 23 | problem: 24 | encrypt_deprecated: discourse-encrypt 插件已弃用,并将于 2025 年第一季度停止提供支持。有关详情,请参阅 Discourse Meta。请卸载该插件以移除此消息。 25 | -------------------------------------------------------------------------------- /config/locales/server.zh_TW.yml: -------------------------------------------------------------------------------- 1 | # WARNING: Never edit this file. 2 | # It will be overwritten when translations are pulled from Crowdin. 3 | # 4 | # To work with us on translations, join this project: 5 | # https://translate.discourse.org/ 6 | 7 | zh_TW: 8 | -------------------------------------------------------------------------------- /config/settings.yml: -------------------------------------------------------------------------------- 1 | plugins: 2 | encrypt_enabled: 3 | default: true 4 | client: true 5 | refresh: true 6 | validator: "EncryptEnabledValidator" 7 | auto_enable_encrypt: 8 | client: true 9 | default: false 10 | refresh: true 11 | encrypt_groups: 12 | default: "" 13 | type: list 14 | list_type: simple 15 | encrypt_pms_default: 16 | client: true 17 | default: false 18 | allow_new_encrypted_pms: 19 | default: true 20 | client: true 21 | allow_decrypting_pms: 22 | default: false 23 | client: true -------------------------------------------------------------------------------- /db/migrate/20190803111542_update_protocol.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class UpdateProtocol < ActiveRecord::Migration[5.2] 4 | def up 5 | execute <<~SQL 6 | INSERT INTO user_custom_fields(user_id, name, value, created_at, updated_at) 7 | WITH public_keys AS ( SELECT user_id, value FROM user_custom_fields WHERE name = 'encrypt_public_key' ), 8 | private_keys AS ( SELECT user_id, value FROM user_custom_fields WHERE name = 'encrypt_private_key' ), 9 | salts AS ( SELECT user_id, value FROM user_custom_fields WHERE name = 'encrypt_salt' ) 10 | SELECT users.id, 11 | 'encrypt_private', 12 | '0$' || public_keys.value || '$' || private_keys.value || '$' || salts.value, 13 | NOW(), 14 | NOW() 15 | FROM users 16 | JOIN public_keys ON users.id = public_keys.user_id 17 | JOIN private_keys ON users.id = private_keys.user_id 18 | JOIN salts ON users.id = salts.user_id 19 | SQL 20 | 21 | execute <<~SQL 22 | UPDATE user_custom_fields 23 | SET name = 'encrypt_public', value = '0$' || value 24 | WHERE name = 'encrypt_public_key' 25 | SQL 26 | 27 | execute <<~SQL 28 | DELETE FROM user_custom_fields 29 | WHERE name IN ('encrypt_public_key', 'encrypt_private_key', 'encrypt_salt') 30 | SQL 31 | 32 | execute <<~SQL 33 | UPDATE topic_custom_fields 34 | SET value = '0$' || value 35 | WHERE name = 'encrypted_title' 36 | SQL 37 | 38 | execute <<~SQL 39 | UPDATE posts 40 | SET raw = '0$' || raw 41 | FROM topic_custom_fields tcf 42 | WHERE posts.topic_id = tcf.topic_id AND 43 | tcf.name = 'encrypted_title' AND 44 | posts.raw ~ '^[A-Za-z0-9+\\\/=$]+(\n.*)?$'; 45 | SQL 46 | end 47 | 48 | def down 49 | raise ActiveRecord::IrreversibleMigration 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /db/migrate/20190916115531_add_paper_keys.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddPaperKeys < ActiveRecord::Migration[5.2] 4 | def up 5 | execute <<~SQL 6 | UPDATE user_custom_fields 7 | SET value = '{"passphrase": "' || value || '"}' 8 | WHERE name = 'encrypt_private' 9 | SQL 10 | end 11 | 12 | def down 13 | raise ActiveRecord::IrreversibleMigration 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /db/migrate/20200129230327_create_encrypted_topics_users.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateEncryptedTopicsUsers < ActiveRecord::Migration[6.0] 4 | def up 5 | create_table :encrypted_topics_users do |t| 6 | t.integer :user_id, index: true 7 | t.integer :topic_id, index: true 8 | t.text :key 9 | end 10 | 11 | add_index :encrypted_topics_users, %i[user_id topic_id], unique: true 12 | 13 | execute <<~SQL 14 | INSERT INTO encrypted_topics_users(user_id, topic_id, key) 15 | SELECT split_part(key, '_', 3)::INTEGER AS user_id, split_part(key, '_', 2)::INTEGER AS topic_id, value 16 | FROM plugin_store_rows 17 | WHERE plugin_name = 'discourse-encrypt' AND key LIKE 'key_%' 18 | SQL 19 | end 20 | 21 | def down 22 | execute <<~SQL 23 | INSERT INTO plugin_store_rows(plugin_name, key, type_name, value) 24 | SELECT 'discourse-encrypt' AS plugin_name, CONCAT('key_', topic_id, '_', user_id) AS key, 'string' AS type_name, key AS value 25 | FROM encrypted_topics_users 26 | SQL 27 | drop_table :encrypted_topics_users 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /db/migrate/20200130050409_create_user_encryption_keys.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateUserEncryptionKeys < ActiveRecord::Migration[6.0] 4 | def up 5 | create_table :user_encryption_keys do |t| 6 | t.integer :user_id, index: true, unique: true 7 | t.text :encrypt_public 8 | t.text :encrypt_private 9 | t.timestamps 10 | end 11 | 12 | execute <<~SQL 13 | INSERT INTO user_encryption_keys(user_id, encrypt_public, created_at, updated_at) 14 | SELECT user_id, value AS encrypt_public, created_at, updated_at 15 | FROM user_custom_fields 16 | WHERE name = 'encrypt_public' 17 | SQL 18 | 19 | execute <<~SQL 20 | UPDATE user_encryption_keys 21 | SET encrypt_private = user_custom_fields.value 22 | FROM user_custom_fields 23 | WHERE user_encryption_keys.user_id = user_custom_fields.user_id AND user_custom_fields.name = 'encrypt_private' 24 | SQL 25 | end 26 | 27 | def down 28 | execute <<~SQL 29 | INSERT INTO user_custom_fields(name, user_id, value, created_at, updated_at) 30 | SELECT 'encrypt_public' AS name, user_id, encrypt_public AS value, created_at, updated_at 31 | FROM user_encryption_keys 32 | SQL 33 | 34 | execute <<~SQL 35 | INSERT INTO user_custom_fields(name, user_id, value, created_at, updated_at) 36 | SELECT 'encrypt_private' AS name, user_id, encrypt_private AS value, created_at, updated_at 37 | FROM user_encryption_keys 38 | SQL 39 | drop_table :user_encryption_keys 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /db/migrate/20200223214818_create_encrypted_topics_data.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateEncryptedTopicsData < ActiveRecord::Migration[6.0] 4 | def up 5 | create_table :encrypted_topics_data do |t| 6 | t.integer :topic_id, index: true 7 | t.text :title 8 | t.timestamps 9 | end 10 | 11 | execute <<~SQL 12 | INSERT INTO encrypted_topics_data(topic_id, title, created_at, updated_at) 13 | SELECT topic_id, value AS title, created_at, updated_at 14 | FROM topic_custom_fields 15 | WHERE name = 'encrypted_title' 16 | SQL 17 | end 18 | 19 | def down 20 | execute <<~SQL 21 | INSERT INTO topic_custom_fields(topic_id, value, name, created_at, updated_at) 22 | SELECT topic_id, title AS value, 'encrypted_title' AS name, created_at, updated_at 23 | FROM encrypted_topics_data 24 | SQL 25 | drop_table :encrypted_topics_data 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /db/migrate/20201027233335_create_encrypted_post_timers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class CreateEncryptedPostTimers < ActiveRecord::Migration[6.0] 4 | def change 5 | create_table :encrypted_post_timers do |t| 6 | t.integer :post_id, null: false 7 | t.datetime :delete_at, null: false 8 | t.datetime :destroyed_at 9 | t.timestamps 10 | end 11 | 12 | add_index :encrypted_post_timers, :post_id 13 | add_index :encrypted_post_timers, :delete_at 14 | add_index :encrypted_post_timers, :destroyed_at 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /db/migrate/20221102045508_add_encrypt_pms_default_to_user_options.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AddEncryptPmsDefaultToUserOptions < ActiveRecord::Migration[6.1] 4 | def up 5 | add_column :user_options, :encrypt_pms_default, :boolean, null: true 6 | 7 | execute "UPDATE user_options SET encrypt_pms_default = #{default_value}" 8 | 9 | change_column :user_options, :encrypt_pms_default, :boolean, null: false 10 | end 11 | 12 | def down 13 | remove_column :user_options, :encrypt_pms_default 14 | end 15 | 16 | def default_value 17 | setting_value = 18 | DB.query_single("SELECT value FROM site_settings WHERE name = 'encrypt_pms_default'").first 19 | setting_value == "t" 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /db/migrate/20221123235603_allow_encrypt_pms_default_to_be_null.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class AllowEncryptPmsDefaultToBeNull < ActiveRecord::Migration[7.0] 4 | def up 5 | change_column_null(:user_options, :encrypt_pms_default, true) 6 | end 7 | 8 | def down 9 | change_column_null(:user_options, :encrypt_pms_default, false) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import DiscourseRecommended from "@discourse/lint-configs/eslint"; 2 | 3 | export default [...DiscourseRecommended]; 4 | -------------------------------------------------------------------------------- /lib/encrypted_post_creator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class EncryptedPostCreator < PostCreator 4 | def create 5 | if encrypt_valid? 6 | topic_key = @opts[:topic_key] || SecureRandom.random_bytes(32) 7 | 8 | # Encrypt title and post contents 9 | @opts[:raw] = EncryptedPostCreator.encrypt(@opts[:raw], topic_key) 10 | if title = @opts[:title] 11 | @opts[:title] = I18n.with_locale(SiteSetting.default_locale) do 12 | I18n.t("js.encrypt.encrypted_title") 13 | end 14 | end 15 | 16 | ret = super 17 | end 18 | 19 | if @post && errors.blank? 20 | # Save the topic key if this is a new topic 21 | if !@opts[:topic_key] 22 | users.each do |user| 23 | key = EncryptedPostCreator.export_key(user, topic_key) 24 | EncryptedTopicsUser.create!(topic_id: @post.topic_id, user_id: user.id, key: key) 25 | end 26 | end 27 | 28 | encrypt_topic_title = EncryptedTopicsData.find_or_initialize_by(topic_id: @post.topic_id) 29 | encrypt_topic_title.update!(title: EncryptedPostCreator.encrypt(title, topic_key)) 30 | end 31 | 32 | ret 33 | end 34 | 35 | def encrypt_valid? 36 | @topic = Topic.find_by(id: @opts[:topic_id]) if @opts[:topic_id] 37 | if @opts[:archetype] != Archetype.private_message && !@topic&.is_encrypted? 38 | errors.add(:base, I18n.t("encrypt.only_pms")) 39 | return false 40 | end 41 | 42 | users.each do |user| 43 | if !user.encrypt_key 44 | errors.add(:base, I18n.t("js.encrypt.composer.user_has_no_key", username: user.username)) 45 | return false 46 | end 47 | end 48 | 49 | true 50 | end 51 | 52 | private 53 | 54 | def users 55 | @users ||= 56 | User 57 | .human_users 58 | .includes(:user_encryption_key) 59 | .where( 60 | username_lower: (@opts[:target_usernames].split(",") << @user.username).map(&:downcase), 61 | ) 62 | .to_a 63 | end 64 | 65 | def self.encrypt(raw, key) 66 | iv = SecureRandom.random_bytes(12) 67 | 68 | cipher = OpenSSL::Cipher.new("aes-256-gcm").encrypt 69 | cipher.key = key 70 | cipher.iv = iv 71 | cipher.auth_data = "" 72 | 73 | plaintext = JSON.dump(raw: raw) 74 | ciphertext = cipher.update(plaintext) 75 | ciphertext += cipher.final 76 | ciphertext += cipher.auth_tag 77 | 78 | "1$#{Base64.strict_encode64(iv)}#{Base64.strict_encode64(ciphertext)}" 79 | end 80 | 81 | def self.export_key(user, topic_key) 82 | Base64.strict_encode64(user.encrypt_key.public_encrypt_oaep256(topic_key)) 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/encrypted_search.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # This class is used only to preload all data needed to display encrypted 4 | # search results. The actual search is performed on the client side. 5 | class EncryptedSearch < Search 6 | # Simplified posts_query that does almost nothing, but fetch visible posts. 7 | # The term is looked up on the client side. 8 | def posts_query(limit, type_filter: nil) 9 | posts = 10 | Post 11 | .includes(topic: :encrypted_topics_data) 12 | .where.not(encrypted_topics_data: { title: nil }) 13 | .joins(topic: :encrypted_topics_users) 14 | .where(encrypted_topics_users: { user_id: @guardian.user&.id }) 15 | .where(post_number: 1) 16 | .where(post_type: Topic.visible_post_types(@guardian.user)) 17 | .where("post_search_data.private_message") 18 | 19 | if @filters 20 | @filters.each do |block, match| 21 | if block.arity == 1 22 | posts = instance_exec(posts, &block) || posts 23 | else 24 | posts = instance_exec(posts, match, &block) || posts 25 | end 26 | end 27 | end 28 | 29 | posts.order("posts.created_at DESC").limit(limit) 30 | end 31 | 32 | # Similar to posts_query does almost nothing other than to return a set of 33 | # posts that might be relevant. 34 | def private_messages_search 35 | raise Discourse::InvalidAccess.new if @guardian.anonymous? 36 | 37 | @search_pms = true # needed by posts_eager_loads 38 | posts = 39 | posts_scope(posts_eager_loads(posts_query(@opts[:limit], type_filter: @opts[:type_filter]))) 40 | posts.each { |post| @results.posts << post } 41 | end 42 | 43 | def posts_scope(default_scope = Post.all) 44 | if SiteSetting.use_pg_headlines_for_excerpt 45 | default_scope.select( 46 | "topics.fancy_title AS topic_title_headline", 47 | "posts.cooked AS headline", 48 | "LEFT(posts.cooked, 50) AS leading_raw_data", 49 | "RIGHT(posts.cooked, 50) AS trailing_raw_data", 50 | default_scope.arel.projections, 51 | ) 52 | else 53 | default_scope 54 | end 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/grouped_search_result_serializer_extension.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DiscourseEncrypt::GroupedSearchResultSerializerExtension 4 | def self.prepended(base) 5 | base.attributes :type_filter 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/openssl.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "openssl" 4 | require "securerandom" 5 | 6 | module OpenSSL 7 | module PKey 8 | class RSA 9 | def public_encrypt_oaep256(msg) 10 | public_encrypt(PKCS1.oaep_mgf1(msg, n.num_bytes), OpenSSL::PKey::RSA::NO_PADDING) 11 | end 12 | end 13 | end 14 | 15 | module PKCS1 16 | # Public-Key Cryptography Standards (PKCS) #1: RSA Cryptography 17 | # Page 18, https://www.ietf.org/rfc/rfc3447 18 | def oaep_mgf1(msg, k) 19 | m_len = msg.bytesize 20 | h_len = OpenSSL::Digest.new("SHA256").digest_length 21 | raise OpenSSL::PKey::RSAError, "message too long" if m_len > k - 2 * h_len - 2 22 | 23 | l_hash = OpenSSL::Digest.digest("SHA256", "") # label = '' 24 | ps = [0] * (k - m_len - 2 * h_len - 2) 25 | db = l_hash + ps.pack("C*") + [1].pack("C") + [msg].pack("a*") 26 | seed = SecureRandom.random_bytes(h_len) 27 | db_mask = mgf1(seed, k - h_len - 1) 28 | masked_db = db.bytes.zip(db_mask).map! { |a, b| a ^ b }.pack("C*") 29 | seed_mask = mgf1(masked_db, h_len) 30 | masked_seed = seed.bytes.zip(seed_mask).map! { |a, b| a ^ b }.pack("C*") 31 | [0, masked_seed, masked_db].pack("Ca*a*") 32 | end 33 | 34 | module_function :oaep_mgf1 35 | 36 | # Public-Key Cryptography Standards (PKCS) #1: RSA Cryptography 37 | # Page 54, https://www.ietf.org/rfc/rfc3447 38 | def mgf1(seed, mask_len) 39 | c = 0 40 | t = [] 41 | while t.size < mask_len 42 | t += OpenSSL::Digest::SHA256.digest([seed, c].pack("a*N")).bytes 43 | c += 1 44 | end 45 | t[0..mask_len] 46 | end 47 | 48 | module_function :mgf1 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /lib/post_actions_controller_extensions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DiscourseEncrypt::PostActionsControllerExtensions 4 | def create 5 | if SiteSetting.encrypt_enabled? 6 | raise Discourse::NotFound if @post.blank? 7 | if !guardian.is_user_a_member_of_encrypted_conversation?(@post.topic) 8 | raise Discourse::InvalidAccess 9 | end 10 | end 11 | super 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/post_extensions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DiscourseEncrypt::PostExtensions 4 | def self.prepended(base) 5 | base.has_one :encrypted_post_timer 6 | end 7 | 8 | def ciphertext 9 | raw.split("\n")[0] || "" 10 | end 11 | 12 | def is_encrypted? 13 | !!(topic&.is_encrypted? && ciphertext.match(%r{\A[A-Za-z0-9+/=$]+\Z})) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/site_setting_extensions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DiscourseEncrypt::SiteSettingExtensions 4 | def authorized_extensions 5 | original_extensions = super 6 | 7 | if SiteSetting.encrypt_enabled? 8 | if extensions = original_extensions.gsub(/[\s\.]+/, "").downcase.split("|") 9 | return (extensions << "encrypted").uniq.join("|") if !extensions.include?("*") 10 | end 11 | end 12 | 13 | original_extensions 14 | end 15 | 16 | def authorized_extensions=(extensions) 17 | super(extensions.split("|").reject { |ext| ext == "encrypted" }.join("|")) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/site_settings_type_supervisor_extensions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DiscourseEncrypt::SiteSettingsTypeSupervisorExtensions 4 | def validate_content_security_policy(value) 5 | super if defined?(super) 6 | 7 | if value == "t" && 8 | !DiscourseEncrypt.safe_csp_src?(SiteSetting.content_security_policy_script_src) && 9 | SiteSetting.encrypt_enabled 10 | raise Discourse::InvalidParameters.new(I18n.t("site_settings.errors.encrypt_unsafe_csp")) 11 | end 12 | end 13 | 14 | def validate_content_security_policy_script_src(value) 15 | super if defined?(super) 16 | 17 | if SiteSetting.content_security_policy? && !DiscourseEncrypt.safe_csp_src?(value) && 18 | SiteSetting.encrypt_enabled 19 | raise Discourse::InvalidParameters.new(I18n.t("site_settings.errors.encrypt_unsafe_csp")) 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/topic_extensions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DiscourseEncrypt::TopicExtensions 4 | def self.prepended(base) 5 | base.has_one :encrypted_topics_data, dependent: :destroy 6 | base.has_many :encrypted_topics_users, dependent: :destroy 7 | end 8 | 9 | def is_encrypted? 10 | !!(private_message? && encrypted_topics_data&.title) 11 | end 12 | 13 | def remove_allowed_user(removed_by, user) 14 | ret = super 15 | EncryptedTopicsUser.delete_by(topic_id: id, user_id: user.id) if ret 16 | ret 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/topic_guardian_extensions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DiscourseEncrypt::TopicGuardianExtension 4 | def can_convert_topic?(topic) 5 | super && !topic.is_encrypted? 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /lib/topic_view_serializer_extension.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DiscourseEncrypt::TopicViewSerializerExtension 4 | def posts 5 | if SiteSetting.encrypt_enabled? && object.topic.is_encrypted? 6 | posts = object.posts.includes(:encrypted_post_timer) 7 | object.instance_variable_set(:@posts, posts) 8 | end 9 | super 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/topics_controller_extensions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DiscourseEncrypt::TopicsControllerExtensions 4 | def show 5 | if SiteSetting.encrypt_enabled? 6 | topic = Topic.find_by(id: (params[:topic_id] || params[:id])) 7 | raise Discourse::InvalidAccess if !guardian.is_user_a_member_of_encrypted_conversation?(topic) 8 | end 9 | super 10 | end 11 | 12 | def update 13 | @topic ||= Topic.find_by(id: params[:topic_id]) 14 | 15 | if @topic&.is_encrypted? && params[:encrypted_title].presence 16 | guardian.ensure_can_edit!(@topic) 17 | @topic.encrypted_topics_data.update!(title: params.delete(:encrypted_title)) 18 | end 19 | 20 | super 21 | end 22 | 23 | def invite 24 | @topic ||= Topic.find_by(id: params[:topic_id]) 25 | 26 | if @topic.is_encrypted? 27 | if params[:key].present? 28 | @user ||= User.find_by_username_or_email(params[:user]) 29 | guardian.ensure_can_invite_to!(@topic) 30 | EncryptedTopicsUser.create!(topic_id: @topic.id, user_id: @user.id, key: params[:key]) 31 | else 32 | return render_json_error(I18n.t("js.encrypt.cannot_invite")) 33 | end 34 | end 35 | 36 | super 37 | end 38 | 39 | def invite_group 40 | @topic ||= Topic.find_by(id: params[:topic_id]) 41 | 42 | return render_json_error(I18n.t("js.encrypt.cannot_invite_group")) if @topic.is_encrypted? 43 | 44 | super 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/upload_validator_extensions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DiscourseEncrypt::UploadValidatorExtensions 4 | def validate(upload) 5 | extension = File.extname(upload.original_filename)[1..-1] || "" 6 | 7 | if extension == "encrypted" 8 | filename = upload.original_filename.gsub(/\.encrypted$/, "") 9 | extension = File.extname(filename)[1..-1] || "" 10 | 11 | if is_authorized?(upload, extension) 12 | if FileHelper.is_supported_image?(filename) 13 | authorized_image_extension(upload, extension) 14 | maximum_image_file_size(upload) 15 | else 16 | authorized_attachment_extension(upload, extension) 17 | maximum_attachment_file_size(upload) 18 | end 19 | end 20 | end 21 | 22 | super 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/user_extensions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DiscourseEncrypt::UserExtensions 4 | def self.prepended(base) 5 | base.has_one :user_encryption_key 6 | end 7 | 8 | def encrypt_key 9 | @encrypt_key ||= 10 | begin 11 | identity = self.user_encryption_key&.encrypt_public 12 | return nil if !identity 13 | 14 | # Check identity version 15 | version, identity = identity.split("$", 2) 16 | return nil if version.to_i != 1 17 | 18 | jwk = JSON.parse(Base64.decode64(identity))["encryptPublic"] 19 | n = OpenSSL::BN.new(Base64.urlsafe_decode64(jwk["n"]), 2) 20 | e = OpenSSL::BN.new(Base64.urlsafe_decode64(jwk["e"]), 2) 21 | 22 | data_sequence = OpenSSL::ASN1.Sequence([OpenSSL::ASN1.Integer(n), OpenSSL::ASN1.Integer(e)]) 23 | OpenSSL::PKey::RSA.new(data_sequence.to_der) 24 | end 25 | end 26 | 27 | def publish_identity 28 | MessageBus.publish( 29 | "/plugin/encrypt/keys", 30 | { 31 | public: self.user_encryption_key&.encrypt_public, 32 | private: self.user_encryption_key&.encrypt_private, 33 | }, 34 | user_ids: [self.id], 35 | ) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/user_notification_renderer_extensions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module DiscourseEncrypt::UserNotificationRendererExtensions 4 | def render(*args) 5 | post = args[0]&.dig(:locals, :post) 6 | args[0][:locals][:in_reply_to_post] = nil if post&.is_encrypted? 7 | super(*args) 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/validators/encrypt_enabled_validator.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class EncryptEnabledValidator 4 | def initialize(opts = {}) 5 | end 6 | 7 | def valid_value?(value) 8 | !SiteSetting.content_security_policy? || 9 | DiscourseEncrypt.safe_csp_src?(SiteSetting.content_security_policy_script_src) || value == "f" 10 | end 11 | 12 | def error_message 13 | I18n.t("site_settings.errors.encrypt_unsafe_csp") 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "devDependencies": { 4 | "@discourse/lint-configs": "2.2.2", 5 | "ember-template-lint": "6.0.0", 6 | "eslint": "9.15.0", 7 | "prettier": "2.8.8" 8 | }, 9 | "engines": { 10 | "node": ">= 18", 11 | "npm": "please-use-pnpm", 12 | "yarn": "please-use-pnpm", 13 | "pnpm": ">= 9" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /spec/fixtures/test_paper_key_1.txt: -------------------------------------------------------------------------------- 1 | WRESTLE SQUARE REGRET INHERIT ANIMAL GIFT CHEESE EXCLUDE WAIT BLIND BLOOD LAWN 2 | -------------------------------------------------------------------------------- /spec/fixtures/test_paper_key_2.txt: -------------------------------------------------------------------------------- 1 | SPECIAL MAPLE PANDA AUTO EXACT CHEST SPEAK SHUFFLE SHOCK COUSIN FROZEN LIBERTY 2 | -------------------------------------------------------------------------------- /spec/fixtures/test_public_key_1.txt: -------------------------------------------------------------------------------- 1 | 1$eyJlbmNyeXB0UHVibGljIjp7ImFsZyI6IlJTQS1PQUVQLTI1NiIsImUiOiJBUUFCIiwiZXh0Ijp0cnVlLCJrZXlfb3BzIjpbImVuY3J5cHQiLCJ3cmFwS2V5Il0sImt0eSI6IlJTQSIsIm4iOiJ2dmdnd19ZWmN4dGdoVndTOEFNZFBxTW1ISElJY2d0NDRXNTRoMF8wYmtORWJqN3pudGk1YVFxakI0U0ptZm14WU9KQVowLXJtemRIYVZHbjV1d21Pb0VzYktVcWN5bWc0YzZtX3NGUFFQdWpPRF9WajZvN2hIOF84MXN4Y196RE1sdks0TkNudDRldHlsbkxjamlza3VIdWRHMlRGY2NkRng2RklZTUlVZFlQTmRhaGRJLTZDMGxaYVVRVXQzbnJEVGV5N0lKRFIyNm9wbllXSExBSFdfTVZUS0c2SkVmdkRBYkE4ZGgwQ0hsSk45ZTVuY19GSXhDblhhRXhmdWdJQThKYlE3S1pSaERkYzlrbUpYV0RBLWp1SDF5R0ZWUFM5VzZJWTdYUmhhVHlIckFHeWt6Y1VIS0JiU3Z0cW1lcHVQM0JjZXk2YlBhRzlWSXprM3lWTGp4dVhWRUY5Njl5MFRHajAtUWVQLXktN0lWdl90MnhqR0hEcHlVUzJHSlVIZjNKTjdYZWVjSWdVZ1JnUURrcGZNN0xpOEIyQnFlWW9tUjY4amFRd1QwSmpEMDJ1UXlNcTRtWDZhSHk0c1ZFZUJCanJfOUVZM3pNRzVxSmdMdUxfbmZIUzFFcVhWUFpSbFVDcDA2ZlZQWWUzZjZhb29EUFRlVmFCdjdZQTNKNnB2aDBrTFpTMGZtaTNLQmY4NVBfX0FVb291eF9TZnZDRjlzeGliWkw4bTY1em56YXVzZVByWHZpUXduQkxRS2xubWZjQ19BRUYtRmhFS3FjOXZhSnJXRGI2bHJUWFN0MXl0WWYwdWV3cGdpT0RCMkdONTh3SW1icjdGWklmOWMwM21pSlZrNGk5VVpfNVZ3dFkzUGhsVlNQVU5Rbl9zcFZJUXI3M0Q3VjNOMCJ9LCJzaWduUHVibGljIjp7ImFsZyI6IlBTMjU2IiwiZSI6IkFRQUIiLCJleHQiOnRydWUsImtleV9vcHMiOlsidmVyaWZ5Il0sImt0eSI6IlJTQSIsIm4iOiJybzg5R2NjVzBZOVFIeHJYLVJvUWROdTdmYV94bE9tWmF1cnBpR0xOMTRRaWJNSk50TFRSUndEdFpXMzN0cEd4UkNUYWcxQzJ4dFV1MktiYTFxYm5MVVJiYnhlaTdiRXNLTHY0TWFJSmpCd3F2enV0TUJ3WGhKRTdSd2RBQ29MM29XbXlva1BGdEloQUtwd1JiTFNLTlVOTEV6bEJvdGs3UGFwQktWTWk4MWNDTFI4WjFWVmRxMjhLQjNRR2dtMG1kWlFFTEZDVHJ6Q0tSc0ZieFlhMm9xYjVaUWFOemZ4bFd5WkprZGlYVFBuQXVHN09udmlrTGx1TnZUZ0d5NzZHQTBiV0RvRkIxNll2SzB2TTd6Wk5aMXYwYjNUSVRVRzkyZnAwXzhrd3lFNXVsaE1tdmxrd2VDZjBMWHR1Ti0xakJySDVJdGtHX3lQRzVHeVZNeFpaMnFzTk9EYTQ5MDZBSFd6SHp0Qm5YNEg3UmFMa0F2OEkxZlRyMWRlc2JzUEgwX1M4NWJyVjI3cEh5YXljM2JGYVdrSHpjbUtBb3RJRHBRU2pOekJYNlpXNFY2dDFQR1FfYy1zMTdQZkFXeF9MVXJLRnU2N2R4VE5wNzBOem9fTEJVelpCY0p2SVppNThfU1BBUVlRdnBxQ3F0N2pXZXcyME94WmtoU3RaR3ZZU0l1MjE1SHc4blFnSVVoalRFREMwUVA3VG9XQ2duUVkzb1lERHg3RmlhWkJsYWI4OU9xc3Mya191ZDh5aG8tcy1xQ2xLYjZRczhIQTJTYW9RNnFkMEFtUHdGNGtGel82SlpPOUkzSV80cWhvTXVJRF9BNkRpR1FqV0I1bkdMUnVsTVJ5RjhZQlZtS09ZTkczN0JHNk9vTGdlOWhzRGRLTjlLUVFWNUpPMUZjcyJ9fQ== 2 | -------------------------------------------------------------------------------- /spec/fixtures/test_public_key_2.txt: -------------------------------------------------------------------------------- 1 | 1$eyJlbmNyeXB0UHVibGljIjp7ImFsZyI6IlJTQS1PQUVQLTI1NiIsImUiOiJBUUFCIiwiZXh0Ijp0cnVlLCJrZXlfb3BzIjpbImVuY3J5cHQiLCJ3cmFwS2V5Il0sImt0eSI6IlJTQSIsIm4iOiJza2lHOVpVZUFQUWZFS01VSk9fTzdSem84Um56VWZGNzdjM0NwSHFoT2dXZGE3RUcxSXYzYTgya0RacTVvcFBFNlZnWWtBSVpQQzhzNmV1OHdoNXEtbjRJZjRFR3RkWnhMaV8xWXZvNnA1cF9qN1Q4eW9EcFhlRExOZnZyVFpsalVBMlJQVTAyX254OE9sS3BRQ3MzRzlHdXo1N19mLWtCQVg4NlRuRWljeElzZ2RKWDk3aEpOamlia2tFSlNCUDN4VHM1TklkcGFtaTViWDJJMl9UR3QxaFZLLTVwM29fRlFWblBtMVkweEpEamU3eEQwZnItbDhRMGtUSUlBVnpubmZCY0kzWFVwaEdzenZLTHE0UDhpNkFZWUstaGRvNVZMSFl3aHZWSmZjeDN1UUpwek1yLWYwNFhrZVBGTFVxYTBxekNtOWpIRm9wQWZob2xZbGh0d250Z0VPTllzMHp6SEJVVHVZU085NnFGR3Z3c0QtUnJBUklKZEdYZFpwVm1JcjJDeC13VnJ5VjhDU3FURTdESWZYa1RRM1pGS3Y4SHZUZnBXWTYtaDN5Njljc1hraTZHZlVsYjZYc3dlVlFvZ1FBUTNBT2RDVzE3cGtmakh0Xzd0b2w4anZQVVYtRExMRllZUW9hTHo5OUFBTnBuWW52ZVZGc1FYWHQzOTRFU1dyMHppbWc2ZGdUZGc1THJ5d2VQMjdjVFlCSndNQ1NKOU0zcWpyQmp5VVdaYXFkYk92a2VhdTZMMkxIcU5GaTFCT3BjejhlWHMyZTlmbndpQzhtQnNyU1RnajVTUW9tejEyWFgyY1UzdXI0SmJzY1BJN2JQM28ycHBnUWlFSGFIc0NhdnNTa0lYNUlfbmJ3MndtbGtDSDFZT3lIM2h4TEprNkdsaTRMQkR0ayJ9LCJzaWduUHVibGljIjp7ImFsZyI6IlBTMjU2IiwiZSI6IkFRQUIiLCJleHQiOnRydWUsImtleV9vcHMiOlsidmVyaWZ5Il0sImt0eSI6IlJTQSIsIm4iOiI1YnVKT3EteEt2MWxGVHc3UEV1cV9HZTRiS3FDLWJyd3ZLVkZfZzF3NUNRQnNDMHpDQld6bjFaeGJIVkVnRlp2YXlDanMwLVMyQXBrb3pGdWxPcVQxdlVRbEpiWXduSlFicmM2ZXA0VlJMTFh6VE1SanVCbkUzUkVzdDdyc0puR25JYUpiZVczNy01cGNGR0lIM1czTURDNGVXUERyamVoOFEtaWVhQkUzd0xFcENCaEQzOVNjTEhqRWw0ODkxbHVxUEJDR0FFU0xudUdlUnFra0N5dTV6LTZtdzlPLXJBaldKLTd4YlFsOWJVaVRtTW5UNTVfcVRHcWpLWDRmUTkyVUo0My1HcE9QZ2VXcE9fQ0FaYzdtdDVsU0hBdW91WFlJUVFVZTB4VXFZOUZ2NmotOFY1MjZpVkZsX0NDdDAtRXN4a2hpMnU3YmtucmhoTHVHbGlCVTVuMW5hYkV1SkZobHNpUnJiWElLZnZlalZ0N0xTTGpEM19DNmlVeHBjTEZweG9vN3ZXQXgxM0kyeEN3NUJEUDdUSmJvQS0zeDlvRk14N3I1MG0tQll0T3BodFc5X2dtRE0wQm10eEptVTZYOEFHSUllcnhBVTcyWENZb0xQUkdlZUpmOTRaV2VxZW9JUjEwRWRXWE14R2xOOGlIbXRFSzZqNmhQcGFicWg3Ty15VkhmQW9ITW5nODQ0NDFhNk1aQm9FOXBiWXJSbmhlNzJCc0hZbGlpYmhkbzl6bE5aZ2FlcTdDX19UZlI3Qk9GVDBjc3g5bDRDRTZtbi1WbG0zTlZ3VTUzdFFQWWFSSDBzRGpXakJ3NVk5MERBc2IxUno4UTMwb180VndNLWROYWhpd1Q1bGdNc1ZMeEVNam41NWVaUm1UWjMwNFZjVi1ZTUljVGR6T1FhVSJ9fQ== 2 | -------------------------------------------------------------------------------- /spec/jobs/encrypt_consistency_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails_helper" 4 | 5 | describe Jobs::EncryptConsistency do 6 | subject(:job) { described_class.new } 7 | 8 | fab!(:topic) { Fabricate(:encrypt_topic) } 9 | fab!(:user_without_invite) { Fabricate(:user) } 10 | fab!(:user_without_key) { Fabricate(:user) } 11 | 12 | before do 13 | EncryptedTopicsUser.create!( 14 | topic_id: topic.id, 15 | user_id: user_without_invite.id, 16 | key: "topic key", 17 | ) 18 | TopicAllowedUser.create(topic_id: topic.id, user_id: user_without_key.id) 19 | end 20 | 21 | it "ensures invites and topic keys are consistent" do 22 | expect { job.execute({}) }.to change { 23 | TopicAllowedUser.exists?(topic: topic, user: user_without_invite) 24 | }.from(false).to(true).and change { 25 | TopicAllowedUser.exists?(topic: topic, user: user_without_key) 26 | }.from(true).to(false) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/jobs/encrypted_post_timer_evaluator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails_helper" 4 | 5 | describe Jobs::EncryptedPostTimerEvaluator do 6 | fab!(:topic) { Fabricate(:encrypt_topic) } 7 | fab!(:post1) { Fabricate(:encrypt_post, topic: topic) } 8 | fab!(:post2) { Fabricate(:encrypt_post, topic: topic) } 9 | fab!(:post3) { Fabricate(:encrypt_post, topic: topic) } 10 | 11 | describe "explosion of first post" do 12 | it "when time is right, delete all posts" do 13 | encrypted_post_timer = EncryptedPostTimer.create!(post: post1, delete_at: 1.hour.from_now) 14 | described_class.new.execute({}) 15 | expect(post1.reload.persisted?).to be true 16 | expect(post2.reload.persisted?).to be true 17 | expect(post3.reload.persisted?).to be true 18 | expect(topic.reload.persisted?).to be true 19 | expect(encrypted_post_timer.reload.destroyed_at).to be nil 20 | 21 | freeze_time 61.minutes.from_now 22 | described_class.new.execute({}) 23 | expect { post1.reload }.to raise_error(ActiveRecord::RecordNotFound) 24 | expect { post2.reload }.to raise_error(ActiveRecord::RecordNotFound) 25 | expect { post3.reload }.to raise_error(ActiveRecord::RecordNotFound) 26 | expect { topic.reload }.to raise_error(ActiveRecord::RecordNotFound) 27 | expect(encrypted_post_timer.reload.destroyed_at).not_to be nil 28 | end 29 | end 30 | 31 | describe "explosion of consecutive posts" do 32 | it "when time is right, delete only one post" do 33 | encrypted_post_timer = EncryptedPostTimer.create!(post: post2, delete_at: 1.hour.from_now) 34 | encrypted_post_timer2 = EncryptedPostTimer.create!(post: post3, delete_at: 1.hour.from_now) 35 | described_class.new.execute({}) 36 | expect(post1.reload.persisted?).to be true 37 | expect(post2.reload.persisted?).to be true 38 | expect(post3.reload.persisted?).to be true 39 | expect(encrypted_post_timer.reload.destroyed_at).to be nil 40 | expect(encrypted_post_timer2.reload.destroyed_at).to be nil 41 | 42 | freeze_time 61.minutes.from_now 43 | described_class.new.execute({}) 44 | expect(post1.reload.persisted?).to be true 45 | expect { post2.reload }.to raise_error(ActiveRecord::RecordNotFound) 46 | expect { post3.reload }.to raise_error(ActiveRecord::RecordNotFound) 47 | expect(topic.reload.persisted?).to be true 48 | expect(encrypted_post_timer.reload.destroyed_at).not_to be nil 49 | expect(encrypted_post_timer2.reload.destroyed_at).not_to be nil 50 | end 51 | 52 | it "does not error when post is already deleted" do 53 | encrypted_post_timer = EncryptedPostTimer.create!(post_id: -5, delete_at: 1.hour.from_now) 54 | freeze_time 61.minutes.from_now 55 | described_class.new.execute({}) 56 | expect(encrypted_post_timer.reload.destroyed_at).not_to be nil 57 | end 58 | 59 | it "does not error when user is deleted" do 60 | post2.user.destroy 61 | encrypted_post_timer = 62 | EncryptedPostTimer.create!(post_id: post2.id, delete_at: 1.hour.from_now) 63 | freeze_time 61.minutes.from_now 64 | described_class.new.execute({}) 65 | expect(encrypted_post_timer.reload.destroyed_at).not_to be nil 66 | expect { post2.reload }.to raise_error(ActiveRecord::RecordNotFound) 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /spec/lib/site_setting_extensions_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails_helper" 4 | 5 | describe DiscourseEncrypt::SiteSettingExtensions do 6 | let!(:default_extensions) do 7 | SiteSetting.authorized_extensions.split("|").reject { |x| x == "encrypted" } 8 | end 9 | 10 | it "adds 'encrypted' extensions to authorized_extensions" do 11 | expect(SiteSetting.authorized_extensions.split("|")).to match_array( 12 | default_extensions + ["encrypted"], 13 | ) 14 | end 15 | 16 | it "does not add 'encrypted' extensions if * is present" do 17 | SiteSetting.authorized_extensions = "*" 18 | expect(SiteSetting.authorized_extensions.split("|")).to match_array(["*"]) 19 | end 20 | 21 | it "provider does not save 'encrypted' file extensions" do 22 | SiteSetting.authorized_extensions += "|txt" 23 | expect(SiteSetting.authorized_extensions.split("|")).to match_array( 24 | default_extensions + %w[txt encrypted], 25 | ) 26 | expect(SiteSetting.provider.find(:authorized_extensions)&.value&.split("|")).to match_array( 27 | default_extensions + ["txt"], 28 | ) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/lib/upload_validator_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails_helper" 4 | 5 | describe DiscourseEncrypt::UploadValidatorExtensions do 6 | it "removes '.encrypted' extension and validates the real extension" do 7 | SiteSetting.authorized_extensions = "foo" 8 | 9 | expect { Fabricate(:upload, original_filename: "test.foo") }.not_to raise_exception 10 | expect { Fabricate(:upload, original_filename: "test.foo.encrypted") }.not_to raise_exception 11 | expect { Fabricate(:upload, original_filename: "test.bar") }.to raise_exception( 12 | ActiveRecord::RecordInvalid, 13 | ) 14 | expect { Fabricate(:upload, original_filename: "test.bar.encrypted") }.to raise_exception( 15 | ActiveRecord::RecordInvalid, 16 | ) 17 | 18 | SiteSetting.authorized_extensions = "*" 19 | 20 | expect { Fabricate(:upload, original_filename: "test.bar") }.not_to raise_exception 21 | expect { Fabricate(:upload, original_filename: "test.bar.encrypted") }.not_to raise_exception 22 | end 23 | 24 | it "removes '.encrypted' extension and validates the real image extension" do 25 | expect { Fabricate(:upload, original_filename: "test.jpg") }.not_to raise_exception 26 | expect { Fabricate(:upload, original_filename: "test.jpg.encrypted") }.not_to raise_exception 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/models/post_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails_helper" 4 | 5 | describe Post do 6 | let(:post) { Fabricate(:post) } 7 | let(:encrypt_user) { Fabricate(:encrypt_user) } 8 | let(:encrypt_post) { Fabricate(:encrypt_post, user: encrypt_user) } 9 | 10 | describe "#ciphertext" do 11 | it "works" do 12 | ciphertext = "0$ciphertextbase64encoded==" 13 | encrypt_post.update!(raw: "#{ciphertext}\nmetadata maybe?") 14 | 15 | expect(encrypt_post.ciphertext).to eq(ciphertext) 16 | end 17 | end 18 | 19 | describe "#is_encrypted?" do 20 | it "works" do 21 | expect(post.is_encrypted?).to eq(false) 22 | expect(encrypt_post.is_encrypted?).to eq(true) 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/models/topic_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails_helper" 4 | 5 | describe Topic do 6 | let(:topic) { Fabricate(:topic) } 7 | let(:encrypt_post) { Fabricate(:encrypt_post) } 8 | let!(:encrypt_topic) { encrypt_post.topic } 9 | 10 | describe "#is_encrypted?" do 11 | it "works" do 12 | expect(topic.is_encrypted?).to eq(false) 13 | expect(encrypt_topic.is_encrypted?).to eq(true) 14 | end 15 | end 16 | 17 | describe "remove_allowed_user" do 18 | it "deletes topic key for user" do 19 | expect { 20 | encrypt_topic.remove_allowed_user(Discourse.system_user, encrypt_topic.user) 21 | }.to change { TopicAllowedUser.count }.by(-1).and change { EncryptedTopicsUser.count }.by(-1) 22 | expect( 23 | EncryptedTopicsUser.find_by( 24 | topic_id: encrypt_topic.id, 25 | user_id: encrypt_topic.user_id, 26 | )&.key, 27 | ).to eq(nil) 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/models/user_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails_helper" 4 | 5 | describe User do 6 | let(:user) { Fabricate(:user) } 7 | 8 | before { SiteSetting.encrypt_enabled = true } 9 | 10 | describe "user option #encrypt_pms_default" do 11 | it "disabled by default" do 12 | expect(user.user_option.encrypt_pms_default).to eq(false) 13 | end 14 | 15 | it "enabled if site setting value is true" do 16 | SiteSetting.encrypt_pms_default = true 17 | expect(user.user_option.encrypt_pms_default).to eq(true) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/plugin_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails_helper" 4 | 5 | describe ::DiscourseEncrypt do 6 | let(:upload) { Fabricate(:upload) } 7 | let(:post) { Fabricate(:encrypt_post) } 8 | 9 | it "links uploads in encrypted posts" do 10 | Jobs.run_immediately! 11 | 12 | post.update!(raw: "#{post.raw}\n[](#{upload.short_url})") 13 | post.rebake! 14 | 15 | expect(post.uploads).to contain_exactly(upload) 16 | end 17 | 18 | it "can enable encrypt if safe CSP" do 19 | SiteSetting.encrypt_enabled = false # plugin is enabled by default 20 | SiteSetting.content_security_policy_script_src = "'unsafe-eval'" 21 | expect { SiteSetting.encrypt_enabled = true }.not_to raise_error 22 | end 23 | 24 | it "cannot have unsafe CSP if encrypt is enabled" do 25 | SiteSetting.encrypt_enabled = true 26 | expect { 27 | SiteSetting.content_security_policy_script_src = "'unsafe-eval'|'unsafe-inline'" 28 | }.to raise_error(Discourse::InvalidParameters) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/requests/encrypted_post_timers_controller_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails_helper" 4 | 5 | describe DiscourseEncrypt::EncryptedPostTimersController do 6 | let!(:user) { Fabricate(:encrypt_user, admin: true) } 7 | let!(:user2) { Fabricate(:encrypt_user) } 8 | let!(:topic) do 9 | Fabricate( 10 | :encrypt_topic, 11 | user: user, 12 | topic_allowed_users: [Fabricate.build(:topic_allowed_user, user: user)], 13 | ) 14 | end 15 | let!(:encrypt_post) { Fabricate(:encrypt_post, topic: topic) } 16 | 17 | it "creates and deletes timer if user has access to private message" do 18 | sign_in(user2) 19 | post "/encrypt/encrypted_post_timers.json", params: { post_id: topic.posts.first.id } 20 | expect(response.status).to eq(403) 21 | expect(EncryptedPostTimer.count).to eq(0) 22 | 23 | sign_in(user) 24 | post "/encrypt/encrypted_post_timers.json", params: { post_id: topic.posts.first.id } 25 | expect(response).to be_successful 26 | expect(EncryptedPostTimer.count).to eq(1) 27 | 28 | topic.update(deleted_at: Time.now) 29 | sign_in(user2) 30 | delete "/encrypt/encrypted_post_timers.json", params: { post_id: topic.posts.first.id } 31 | expect(response.status).to eq(403) 32 | expect(EncryptedPostTimer.count).to eq(1) 33 | 34 | sign_in(user) 35 | delete "/encrypt/encrypted_post_timers.json", params: { post_id: topic.posts.first.id } 36 | expect(response).to be_successful 37 | expect(EncryptedPostTimer.count).to eq(0) 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/requests/posts_controller_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails_helper" 4 | 5 | describe PostsController do 6 | let(:encrypt_post) { Fabricate(:encrypt_post) } 7 | let(:user) { Fabricate(:user) } 8 | let(:group) { Fabricate(:group) } 9 | 10 | before { sign_in(Fabricate(:admin)) } 11 | 12 | describe "#create" do 13 | it "works" do 14 | post "/posts.json", 15 | params: { 16 | raw: I18n.t("js.encrypt.encrypted_post"), 17 | title: I18n.t("js.encrypt.encrypted_title"), 18 | archetype: Archetype.private_message, 19 | target_recipients: user.username, 20 | draft_key: Draft::NEW_TOPIC, 21 | is_encrypted: true, 22 | encrypted_title: "1$title", 23 | encrypted_raw: encrypt_post.raw, 24 | encrypted_keys: "{\"#{user.username}\":\"topickey\"}", 25 | } 26 | 27 | expect(response.status).to eq(200) 28 | end 29 | 30 | it "raises an error" do 31 | post "/posts.json", 32 | params: { 33 | raw: I18n.t("js.encrypt.encrypted_post"), 34 | title: I18n.t("js.encrypt.encrypted_title"), 35 | archetype: Archetype.private_message, 36 | target_recipients: user.username, 37 | draft_key: Draft::NEW_TOPIC, 38 | is_encrypted: true, 39 | encrypted_title: "1$title", 40 | encrypted_raw: encrypt_post.raw, 41 | } 42 | 43 | expect(response.status).to eq(422) 44 | expect(JSON.parse(response.body)["errors"]).to include(I18n.t("encrypt.no_encrypt_keys")) 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /spec/requests/topics_controller_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails_helper" 4 | 5 | describe TopicsController do 6 | let(:topic) { Fabricate(:encrypt_topic) } 7 | let(:user) { Fabricate(:user) } 8 | let(:group) { Fabricate(:group) } 9 | let(:admin) { Fabricate(:admin) } 10 | let(:admin2) { Fabricate(:admin) } 11 | 12 | before do 13 | TopicAllowedUser.create!(user_id: admin.id, topic_id: topic.id) 14 | sign_in(admin) 15 | end 16 | 17 | describe "#update" do 18 | it "updates encrypted title" do 19 | put "/t/#{topic.slug}/#{topic.id}.json", params: { encrypted_title: "new encrypted title" } 20 | 21 | expect(response.status).to eq(200) 22 | expect(topic.reload.encrypted_topics_data.title).to eq("new encrypted title") 23 | end 24 | 25 | it "returns invalid access for deleted topic" do 26 | topic.destroy! 27 | put "/t/#{topic.slug}/#{topic.id}.json", params: { encrypted_title: "new encrypted title" } 28 | expect(response.status).to eq(403) 29 | end 30 | end 31 | 32 | it "not invited admin does not have access" do 33 | sign_in(admin2) 34 | get "/t/#{topic.slug}/#{topic.id}.json" 35 | expect(response.status).to eq(403) 36 | 37 | TopicAllowedUser.create!(user_id: admin2.id, topic_id: topic.id) 38 | get "/t/#{topic.slug}/#{topic.id}.json" 39 | expect(response.status).to eq(200) 40 | end 41 | 42 | describe "#invite" do 43 | it "saves user key" do 44 | post "/t/#{topic.id}/invite.json", params: { user: user.username, key: "key of user" } 45 | 46 | expect(response.status).to eq(200) 47 | expect(TopicAllowedUser.where(user_id: user.id, topic_id: topic.id).exists?).to eq(true) 48 | expect(EncryptedTopicsUser.find_by(topic_id: topic.id, user_id: user.id).key).to eq( 49 | "key of user", 50 | ) 51 | end 52 | 53 | it "returns an error with no key" do 54 | post "/t/#{topic.id}/invite.json", params: { user: user.username } 55 | 56 | expect(response.status).to eq(422) 57 | expect(TopicAllowedUser.where(user_id: user.id, topic_id: topic.id).exists?).to eq(false) 58 | expect(EncryptedTopicsUser.where(topic_id: topic.id, user_id: user.id).exists?).to eq(false) 59 | end 60 | end 61 | 62 | describe "#invite_group" do 63 | it "returns an error with no key" do 64 | post "/t/#{topic.id}/invite-group.json", params: { group: group.name } 65 | 66 | expect(response.status).to eq(422) 67 | expect(TopicAllowedGroup.where(group_id: group.id, topic_id: topic.id).exists?).to eq(false) 68 | end 69 | end 70 | 71 | describe "#remove_allowed_user" do 72 | let(:topic) { Fabricate(:encrypt_topic, user: user) } 73 | let(:other_user) { topic.topic_allowed_users.map(&:user).find { |u| u != user } } 74 | 75 | it "uninvites the user" do 76 | put "/t/#{topic.id}/remove-allowed-user.json", params: { username: other_user.username } 77 | 78 | expect(EncryptedTopicsUser.where(topic_id: topic.id, user_id: user.id).exists?).to eq(true) 79 | expect(EncryptedTopicsUser.where(topic_id: topic.id, user_id: other_user.id).exists?).to eq( 80 | false, 81 | ) 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /spec/serializers/current_user_serialier_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails_helper" 4 | 5 | describe CurrentUserSerializer do 6 | let(:user) { Fabricate(:user) } 7 | 8 | it "contains public, private key and encrypt_pms_default" do 9 | UserEncryptionKey.create!( 10 | user_id: user.id, 11 | encrypt_public: "public key", 12 | encrypt_private: "private key", 13 | ) 14 | SiteSetting.encrypt_pms_default = true 15 | serialized = described_class.new(user, scope: Guardian.new(user), root: false).as_json 16 | expect(serialized[:encrypt_public]).to eq("public key") 17 | expect(serialized[:encrypt_private]).to eq("private key") 18 | expect(serialized[:encrypt_pms_default]).to be true 19 | end 20 | 21 | it "use SiteSetting as default when encrypt_pms_default is not set" do 22 | user.user_option.update!(encrypt_pms_default: nil) 23 | UserEncryptionKey.create!( 24 | user_id: user.id, 25 | encrypt_public: "public key", 26 | encrypt_private: "private key", 27 | ) 28 | serialized = described_class.new(user, scope: Guardian.new(user), root: false).as_json 29 | expect(serialized[:encrypt_pms_default]).to be false 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/serializers/topic_serializers_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails_helper" 4 | 5 | [BasicTopicSerializer, ListableTopicSerializer, TopicListItemSerializer].each do |klass| 6 | describe klass do 7 | let(:user) { Fabricate(:user) } 8 | 9 | let(:encrypt_topic) do 10 | Fabricate( 11 | :encrypt_topic, 12 | topic_allowed_users: [Fabricate.build(:topic_allowed_user, user: user)], 13 | ) 14 | end 15 | let(:topic) do 16 | Fabricate( 17 | :private_message_topic, 18 | topic_allowed_users: [Fabricate.build(:topic_allowed_user, user: user)], 19 | ) 20 | end 21 | 22 | it "contains encrypted fields only for encrypted topics" do 23 | serialized = 24 | described_class.new(encrypt_topic, scope: Guardian.new(user), root: false).as_json 25 | expect(serialized[:encrypted_title]).not_to eq(nil) 26 | expect(serialized[:topic_key]).not_to eq(nil) 27 | 28 | serialized = described_class.new(topic, scope: Guardian.new(user), root: false).as_json 29 | expect(serialized[:encrypted_title]).to eq(nil) 30 | expect(serialized[:topic_key]).to eq(nil) 31 | 32 | serialized = described_class.new(encrypt_topic, scope: Guardian.new, root: false).as_json 33 | expect(serialized[:encrypted_title]).to eq(nil) 34 | expect(serialized[:topic_key]).to eq(nil) 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/serializers/topic_view_serializer_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "rails_helper" 4 | 5 | describe TopicViewSerializer do 6 | let(:user) { Fabricate(:user) } 7 | 8 | let(:encrypt_topic) do 9 | Fabricate( 10 | :encrypt_topic, 11 | topic_allowed_users: [Fabricate.build(:topic_allowed_user, user: user)], 12 | ) 13 | end 14 | let(:encrypt_post) { Fabricate(:encrypt_post, topic: encrypt_topic) } 15 | let(:topic) do 16 | Fabricate( 17 | :private_message_topic, 18 | topic_allowed_users: [Fabricate.build(:topic_allowed_user, user: user)], 19 | ) 20 | end 21 | 22 | let(:encrypt_topic_view) { TopicView.new(encrypt_topic.id, user) } 23 | let(:topic_view) { TopicView.new(topic.id, user) } 24 | 25 | it "contains encrypted fields only for encrypted topics" do 26 | EncryptedPostTimer.create!(post: encrypt_post, delete_at: 1.hour.from_now) 27 | 28 | serialized = 29 | described_class.new(encrypt_topic_view, scope: Guardian.new(user), root: false).as_json 30 | expect(serialized[:encrypted_title]).not_to eq(nil) 31 | expect(serialized[:topic_key]).not_to eq(nil) 32 | expect(serialized[:delete_at]).not_to eq(nil) 33 | 34 | serialized = described_class.new(topic_view, scope: Guardian.new(user), root: false).as_json 35 | expect(serialized[:encrypted_title]).to eq(nil) 36 | expect(serialized[:topic_key]).to eq(nil) 37 | expect(serialized[:delete_at]).to eq(nil) 38 | 39 | serialized = described_class.new(encrypt_topic_view, scope: Guardian.new, root: false).as_json 40 | expect(serialized[:encrypted_title]).to eq(nil) 41 | expect(serialized[:topic_key]).to eq(nil) 42 | expect(serialized[:delete_at]).to eq(nil) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/services/problem_check/unsafe_csp_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe ProblemCheck::UnsafeCsp do 4 | let(:check) { described_class.new } 5 | 6 | context "when encryption is not enabled" do 7 | before { SiteSetting.stubs(encrypt_enabled?: false) } 8 | 9 | it { expect(check).to be_chill_about_it } 10 | end 11 | 12 | context "when no CSP is configured" do 13 | before { SiteSetting.stubs(content_security_policy?: false) } 14 | 15 | it { expect(check).to be_chill_about_it } 16 | end 17 | 18 | context "when using a safe CSP configuration" do 19 | before do 20 | SiteSetting.stubs(encrypt_enabled?: true) 21 | SiteSetting.stubs(content_security_policy?: true) 22 | SiteSetting.stubs(content_security_policy_script_src: "script-src") 23 | end 24 | 25 | it { expect(check).to be_chill_about_it } 26 | end 27 | 28 | context "when using an unsafe CSP configuration" do 29 | before do 30 | SiteSetting.stubs(encrypt_enabled?: true) 31 | SiteSetting.stubs(content_security_policy?: true) 32 | SiteSetting.stubs(content_security_policy_script_src: "script-src 'unsafe-inline'") 33 | end 34 | 35 | it do 36 | expect(check).to have_a_problem.with_priority("low").with_message( 37 | "Unsafe CSP directives like 'unsafe-eval' and 'unsafe-inline' cannot be used when the Discourse Encrypt plugin is enabled.", 38 | ) 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /spec/system/decrypt_encrypted_topic_posts_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe "Encrypt | Decypting topic posts", type: :system do 4 | fab!(:current_user) { Fabricate(:user) } 5 | 6 | let(:user_preferences_page) { PageObjects::Pages::UserPreferences.new } 7 | let(:topic_page) { PageObjects::Pages::Topic.new } 8 | let(:topic_title) { "This is a new topic for encryption" } 9 | 10 | before do 11 | encrypt_system_bootstrap(current_user) 12 | sign_in(current_user) 13 | enable_encrypt_with_keys_for_user(current_user) 14 | activate_encrypt(user_preferences_page, current_user) 15 | end 16 | 17 | def select_other_user_for_pm 18 | find("#private-message-users").click 19 | find("#private-message-users-filter input[name='filter-input-search']").send_keys( 20 | other_user.username, 21 | ) 22 | find(".email-group-user-chooser-row").click 23 | end 24 | 25 | describe "with hashtags" do 26 | fab!(:category) { Fabricate(:category, name: "TODO", slug: "todo") } 27 | fab!(:tag) { Fabricate(:tag, name: "bugs") } 28 | fab!(:other_user) { Fabricate(:user) } 29 | 30 | it "decrypts the post" do 31 | enable_encrypt_for_user_in_session(other_user, user_preferences_page) 32 | 33 | topic_page.open_new_message 34 | expect(page).to have_css(".encrypt-controls .d-icon-lock") 35 | 36 | select_other_user_for_pm 37 | 38 | topic_page.fill_in_composer_title(topic_title) 39 | topic_page.fill_in_composer("Here are some hashtags for decryption later on #todo #bugs") 40 | topic_page.send_reply 41 | 42 | # encryption loading and processing takes a little longer than usual 43 | using_wait_time(5) do 44 | expect(find(".fancy-title")).to have_content(topic_title) 45 | expect(page).to have_css(".topic-status .d-icon-user-secret") 46 | expect(page).to have_content("Here are some hashtags for decryption later") 47 | end 48 | 49 | # make sure hashtags are rendered by the post decrypter 50 | expect(page).to have_css(".hashtag-cooked[data-slug='todo']") 51 | expect(page).to have_css(".hashtag-cooked[data-slug='bugs']") 52 | end 53 | end 54 | 55 | describe "with mentions" do 56 | fab!(:user_2) { Fabricate(:user) } 57 | fab!(:user_3) { Fabricate(:user) } 58 | fab!(:other_user) { Fabricate(:user) } 59 | 60 | it "decrypts the post" do 61 | enable_encrypt_for_user_in_session(other_user, user_preferences_page) 62 | 63 | topic_page.open_new_message 64 | expect(page).to have_css(".encrypt-controls .d-icon-lock") 65 | 66 | select_other_user_for_pm 67 | 68 | topic_page.fill_in_composer_title(topic_title) 69 | topic_page.fill_in_composer( 70 | "Here are some mentions for decryption later on @#{user_2.username} @#{user_3.username}", 71 | ) 72 | topic_page.send_reply 73 | 74 | # encryption loading and processing takes a little longer than usual 75 | using_wait_time(5) do 76 | expect(find(".fancy-title")).to have_content(topic_title) 77 | expect(page).to have_css(".topic-status .d-icon-user-secret") 78 | expect(page).to have_content("Here are some mentions for decryption later") 79 | end 80 | 81 | # make sure mentions are rendered by the post decrypter 82 | expect(page).to have_css(".mention[href='/u/#{user_2.username}']") 83 | expect(page).to have_css(".mention[href='/u/#{user_3.username}']") 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /spec/system/enable_encryption_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe "Encrypt | Enabling encrypted messages", type: :system do 4 | fab!(:current_user) { Fabricate(:user) } 5 | before do 6 | encrypt_system_bootstrap(current_user) 7 | sign_in(current_user) 8 | end 9 | 10 | let(:user_preferences_page) { PageObjects::Pages::UserPreferences.new } 11 | 12 | it "shows warning about paper keys when encryption is enabled" do 13 | user_preferences_page.visit(current_user) 14 | click_link "Security" 15 | find("#enable-encrypted-messages").click 16 | using_wait_time(20) do 17 | expect(page).to have_content(I18n.t("js.encrypt.no_backup_warn")[0..100]) 18 | end 19 | expect(current_user.reload.user_encryption_key.encrypt_public).not_to eq(nil) 20 | expect(current_user.reload.user_encryption_key.encrypt_private).to eq(nil) 21 | end 22 | 23 | it "enables encryption and generates paper keys" do 24 | user_preferences_page.visit(current_user) 25 | click_link "Security" 26 | find("#enable-encrypted-messages").click 27 | using_wait_time(5) { expect(page).to have_content(I18n.t("js.encrypt.no_backup_warn")[0..100]) } 28 | expect(current_user.reload.user_encryption_key.encrypt_public).not_to eq(nil) 29 | find("#encrypt-generate-paper-key").click 30 | expect(page).to have_css(".generate-paper-key-modal .paper-key") 31 | paper_key = find(".generate-paper-key-modal .paper-key").text 32 | expect(paper_key).not_to eq(nil) 33 | try_until_success do 34 | expect(current_user.reload.user_encryption_key.encrypt_private).not_to eq(nil) 35 | end 36 | end 37 | 38 | it "activates encrypted messages on the device" do 39 | enable_encrypt_with_keys_for_user(current_user) 40 | user_preferences_page.visit(current_user) 41 | click_link "Security" 42 | expect(page).to have_content(I18n.t("js.encrypt.preferences.status_enabled_but_inactive")) 43 | find("#passphrase").fill_in(with: test_paper_key) 44 | find("#encrypt-activate").click 45 | expect(page).to have_content(I18n.t("js.encrypt.preferences.status_enabled")) 46 | expect(page.execute_script("return localStorage[\"discourse-encrypt\"]")).to eq("true") 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /svg-icons/plugin-icons.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /test/javascripts/lib/base64-test.js: -------------------------------------------------------------------------------- 1 | import QUnit, { module, test } from "qunit"; 2 | import { 3 | base64ToBuffer, 4 | bufferToBase64, 5 | } from "discourse/plugins/discourse-encrypt/lib/base64"; 6 | 7 | /* 8 | * Checks if two array-like objects are equal. 9 | * 10 | * @param haystack 11 | * @param needle 12 | * @param message 13 | */ 14 | QUnit.assert.arrayEqual = function (actual, expected) { 15 | if (actual.length !== expected.length) { 16 | this.pushResult({ 17 | result: false, 18 | actual: actual.length, 19 | expected: expected.length, 20 | message: "array lengths are equal", 21 | }); 22 | 23 | return; 24 | } 25 | 26 | let result = true; 27 | 28 | for (let i = 0; i < actual.length; ++i) { 29 | if (actual[i] !== expected[i]) { 30 | result = false; 31 | this.pushResult({ 32 | result, 33 | actual: actual[i], 34 | expected: expected[i], 35 | message: `index ${i} matches`, 36 | }); 37 | } 38 | } 39 | 40 | if (result) { 41 | this.pushResult({ 42 | result, 43 | actual, 44 | expected, 45 | message: "arrays match", 46 | }); 47 | } 48 | }; 49 | 50 | module("discourse-encrypt:lib:base64", function () { 51 | test("base64 to buffer", function (assert) { 52 | let check = (actual, expected) => 53 | assert.arrayEqual(base64ToBuffer(actual), expected); 54 | 55 | check("", []); 56 | check("QQ==", [0x41]); 57 | check("QUI=", [0x41, 0x42]); 58 | check("QUJD", [0x41, 0x42, 0x43]); 59 | check("QUJDRA==", [0x41, 0x42, 0x43, 0x44]); 60 | }); 61 | 62 | test("buffer to base64", function (assert) { 63 | let check = (actual, expected) => 64 | assert.strictEqual(bufferToBase64(new Uint8Array(actual)), expected); 65 | 66 | check([], ""); 67 | check([0x41], "QQ=="); 68 | check([0x41, 0x42], "QUI="); 69 | check([0x41, 0x42, 0x43], "QUJD"); 70 | check([0x41, 0x42, 0x43, 0x44], "QUJDRA=="); 71 | }); 72 | 73 | test("buffer to base64 to buffer", function (assert) { 74 | const array = []; 75 | for (let i = 0; i < 32; ++i) { 76 | const buffer = new Uint8Array(array); 77 | assert.arrayEqual(base64ToBuffer(bufferToBase64(buffer)), buffer); 78 | array.push(i); 79 | } 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /test/javascripts/lib/database-safari-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from "qunit"; 2 | import { Promise } from "rsvp"; 3 | import { 4 | deleteDb, 5 | loadDbIdentity, 6 | setIndexedDb, 7 | setUserAgent, 8 | } from "discourse/plugins/discourse-encrypt/lib/database"; 9 | 10 | let indexedDbCalls = 0; 11 | 12 | module("discourse-encrypt:lib:database-safari", function (hooks) { 13 | hooks.beforeEach(function () { 14 | indexedDbCalls = 0; 15 | 16 | setIndexedDb({ 17 | open(name, version) { 18 | return window.indexedDB.open(name, version); 19 | }, 20 | 21 | databases() { 22 | return indexedDbCalls++ > 3 23 | ? window.indexedDB.databases() 24 | : new Promise(() => {}); 25 | }, 26 | 27 | deleteDatabase(name) { 28 | indexedDbCalls++; 29 | return window.indexedDB.deleteDatabase(name); 30 | }, 31 | }); 32 | 33 | setUserAgent("iPhone"); 34 | }); 35 | 36 | hooks.afterEach(function () { 37 | setIndexedDb(window.indexedDB); 38 | setUserAgent(window.navigator.userAgent); 39 | }); 40 | 41 | test("IndexedDB is initialized in Safari", async function (assert) { 42 | await deleteDb(); 43 | assert.rejects(loadDbIdentity()); 44 | assert.true(indexedDbCalls > 0); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/javascripts/lib/database-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from "qunit"; 2 | import { 3 | DB_NAME, 4 | deleteDb, 5 | loadDbIdentity, 6 | saveDbIdentity, 7 | setUseLocalStorage, 8 | } from "discourse/plugins/discourse-encrypt/lib/database"; 9 | import { generateIdentity } from "discourse/plugins/discourse-encrypt/lib/protocol"; 10 | 11 | module("discourse-encrypt:lib:database", function () { 12 | test("IndexedDB backend", async function (assert) { 13 | setUseLocalStorage(false); 14 | await deleteDb(); 15 | 16 | assert.rejects(loadDbIdentity()); 17 | 18 | await generateIdentity().then((id) => saveDbIdentity(id)); 19 | assert.true(window.localStorage.getItem(DB_NAME).length > 0); 20 | 21 | const identity = await loadDbIdentity(); 22 | assert.true(identity.encryptPublic instanceof CryptoKey); 23 | assert.true(identity.encryptPrivate instanceof CryptoKey); 24 | assert.true(identity.signPublic instanceof CryptoKey); 25 | assert.true(identity.signPrivate instanceof CryptoKey); 26 | 27 | await deleteDb(); 28 | 29 | assert.rejects(loadDbIdentity()); 30 | assert.strictEqual(window.localStorage.getItem(DB_NAME), null); 31 | }); 32 | 33 | test("Web Storage (localStorage) backend", async function (assert) { 34 | setUseLocalStorage(true); 35 | await deleteDb(); 36 | 37 | assert.rejects(loadDbIdentity()); 38 | 39 | await generateIdentity().then((id) => saveDbIdentity(id)); 40 | assert.true(window.localStorage.getItem(DB_NAME).length > 0); 41 | 42 | const identity = await loadDbIdentity(); 43 | assert.true(identity.encryptPublic instanceof CryptoKey); 44 | assert.true(identity.encryptPrivate instanceof CryptoKey); 45 | assert.true(identity.signPublic instanceof CryptoKey); 46 | assert.true(identity.signPrivate instanceof CryptoKey); 47 | 48 | await deleteDb(); 49 | 50 | assert.rejects(loadDbIdentity()); 51 | assert.strictEqual(window.localStorage.getItem(DB_NAME), null); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /test/javascripts/lib/protocol-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from "qunit"; 2 | import { 3 | decrypt, 4 | encrypt, 5 | exportKey, 6 | generateIdentity, 7 | generateKey, 8 | importKey, 9 | } from "discourse/plugins/discourse-encrypt/lib/protocol"; 10 | 11 | module("discourse-encrypt:lib:protocol", function () { 12 | test("generateKey", async function (assert) { 13 | const key = await generateKey(); 14 | assert.true(key instanceof CryptoKey); 15 | }); 16 | 17 | test("exportKey & importKey", async function (assert) { 18 | const { encryptPublic, encryptPrivate } = await generateIdentity(); 19 | const key = await generateKey(); 20 | const exported = await exportKey(key, encryptPublic); 21 | assert.true( 22 | (await importKey(exported, encryptPrivate)) instanceof CryptoKey 23 | ); 24 | }); 25 | 26 | test("encrypt & decrypt", async function (assert) { 27 | const key = await generateKey(); 28 | const plaintext = "this is a message"; 29 | const ciphertext = await encrypt(key, plaintext); 30 | 31 | assert.strictEqual(plaintext, await decrypt(key, ciphertext)); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /test/javascripts/lib/protocol-v0-test.js: -------------------------------------------------------------------------------- 1 | import QUnit, { module, test } from "qunit"; 2 | import { 3 | _bufferToString, 4 | _exportPrivateKey, 5 | _exportPublicKey, 6 | _getPassphraseKey, 7 | _getSalt, 8 | _importPrivateKey, 9 | _importPublicKey, 10 | _stringToBuffer, 11 | generateIdentity, 12 | } from "discourse/plugins/discourse-encrypt/lib/protocol-v0"; 13 | 14 | /* 15 | * Checks if two array-like objects are equal. 16 | * 17 | * @param haystack 18 | * @param needle 19 | * @param message 20 | */ 21 | QUnit.assert.arrayEqual = function (actual, expected) { 22 | if (actual.length !== expected.length) { 23 | this.pushResult({ 24 | result: false, 25 | actual: actual.length, 26 | expected: expected.length, 27 | message: "array lengths are not equal", 28 | }); 29 | 30 | return; 31 | } 32 | 33 | let result = true; 34 | 35 | for (let i = 0; i < actual.length; ++i) { 36 | if (actual[i] !== expected[i]) { 37 | result = false; 38 | this.pushResult({ 39 | result, 40 | actual: actual[i], 41 | expected: expected[i], 42 | message: `index ${i} mismatches`, 43 | }); 44 | } 45 | } 46 | 47 | if (result) { 48 | this.pushResult({ 49 | result, 50 | actual, 51 | expected, 52 | message: "arrays match", 53 | }); 54 | } 55 | }; 56 | 57 | module("discourse-encrypt:lib:protocol_v0", function () { 58 | test("string to buffer", function (assert) { 59 | let check = (actual, expected) => 60 | assert.arrayEqual(new Uint16Array(_stringToBuffer(actual)), expected); 61 | 62 | check("", []); 63 | check("A", [0x41]); 64 | check("AB", [0x41, 0x42]); 65 | check("ABC", [0x41, 0x42, 0x43]); 66 | check("ABCD", [0x41, 0x42, 0x43, 0x44]); 67 | }); 68 | 69 | test("buffer to string", function (assert) { 70 | let check = (actual, expected) => 71 | assert.strictEqual(_bufferToString(new Uint16Array(actual)), expected); 72 | 73 | check([], ""); 74 | check([0x41], "A"); 75 | check([0x41, 0x42], "AB"); 76 | check([0x41, 0x42, 0x43], "ABC"); 77 | check([0x41, 0x42, 0x43, 0x44], "ABCD"); 78 | }); 79 | 80 | test("buffer to string to buffer", function (assert) { 81 | const array = []; 82 | for (let i = 0; i < 32; ++i) { 83 | const expected = new Uint16Array(array); 84 | assert.arrayEqual( 85 | new Uint16Array(_stringToBuffer(_bufferToString(expected))), 86 | expected 87 | ); 88 | array.push(i); 89 | } 90 | }); 91 | 92 | test("_exportPublicKey & _importPublicKey", async function (assert) { 93 | const { publicKey } = await generateIdentity(); 94 | const exported = await _exportPublicKey(publicKey); 95 | assert.true((await _importPublicKey(exported)) instanceof CryptoKey); 96 | }); 97 | 98 | test("_exportPrivateKey & _importPrivateKey", async function (assert) { 99 | const key = await _getPassphraseKey("passphrase", _getSalt()); 100 | const { privateKey } = await generateIdentity(); 101 | const exported = await _exportPrivateKey(privateKey, key); 102 | assert.true((await _importPrivateKey(exported, key)) instanceof CryptoKey); 103 | }); 104 | 105 | test("_getPassphraseKey", async function (assert) { 106 | const key = await _getPassphraseKey("passphrase", _getSalt()); 107 | assert.true(key instanceof CryptoKey); 108 | }); 109 | 110 | test("_getSalt", async function (assert) { 111 | assert.strictEqual(_getSalt().length, 24); 112 | }); 113 | 114 | test("generateIdentity", async function (assert) { 115 | const { publicKey, privateKey } = await generateIdentity(); 116 | assert.true(publicKey instanceof CryptoKey); 117 | assert.true(privateKey instanceof CryptoKey); 118 | }); 119 | }); 120 | -------------------------------------------------------------------------------- /test/javascripts/lib/protocol-v1-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from "qunit"; 2 | import { 3 | exportIdentity, 4 | generateIdentity, 5 | importIdentity, 6 | } from "discourse/plugins/discourse-encrypt/lib/protocol-v1"; 7 | 8 | module("discourse-encrypt:lib:protocol_v1", function () { 9 | test("generateIdentity", async function (assert) { 10 | const { encryptPublic, encryptPrivate, signPublic, signPrivate } = 11 | await generateIdentity(); 12 | 13 | assert.true(encryptPublic instanceof CryptoKey); 14 | assert.true(encryptPrivate instanceof CryptoKey); 15 | assert.true(signPublic instanceof CryptoKey); 16 | assert.true(signPrivate instanceof CryptoKey); 17 | }); 18 | 19 | test("exportIdentity & importIdentity", async function (assert) { 20 | const identity = await generateIdentity(); 21 | 22 | let exported = await exportIdentity(identity); 23 | let imported = await importIdentity(exported.private); 24 | assert.true(imported.encryptPublic instanceof CryptoKey); 25 | assert.true(imported.encryptPrivate instanceof CryptoKey); 26 | assert.true(imported.signPublic instanceof CryptoKey); 27 | assert.true(imported.signPrivate instanceof CryptoKey); 28 | imported = await importIdentity(exported.public); 29 | assert.true(imported.encryptPublic instanceof CryptoKey); 30 | assert.strictEqual(imported.encryptPrivate, undefined); 31 | assert.true(imported.signPublic instanceof CryptoKey); 32 | assert.strictEqual(imported.signPrivate, undefined); 33 | 34 | exported = await exportIdentity(identity, "test"); 35 | imported = await importIdentity(exported.private, "test"); 36 | assert.true(imported.encryptPublic instanceof CryptoKey); 37 | assert.true(imported.encryptPrivate instanceof CryptoKey); 38 | assert.true(imported.signPublic instanceof CryptoKey); 39 | assert.true(imported.signPrivate instanceof CryptoKey); 40 | imported = await importIdentity(exported.public); 41 | assert.true(imported.encryptPublic instanceof CryptoKey); 42 | assert.strictEqual(imported.encryptPrivate, undefined); 43 | assert.true(imported.signPublic instanceof CryptoKey); 44 | assert.strictEqual(imported.signPrivate, undefined); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /test/javascripts/lib/uploads-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from "qunit"; 2 | import { base64ToBuffer } from "discourse/plugins/discourse-encrypt/lib/base64"; 3 | import { getMetadata } from "discourse/plugins/discourse-encrypt/lib/uploads"; 4 | 5 | const TEST_IMG_BASE64 = 6 | "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="; 7 | const SITE_SETTINGS = { max_image_width: 100, max_image_height: 100 }; 8 | 9 | module("discourse-encrypt:lib:uploadHandler", function () { 10 | test("getMetadata - image file", async function (assert) { 11 | const file = new File([base64ToBuffer(TEST_IMG_BASE64)], "test.png", { 12 | type: "image/png", 13 | }); 14 | const data = await getMetadata(file, SITE_SETTINGS); 15 | assert.strictEqual(data.original_filename, "test.png"); 16 | assert.strictEqual(data.width, 1); 17 | assert.strictEqual(data.height, 1); 18 | assert.strictEqual(data.thumbnail_width, 1); 19 | assert.strictEqual(data.thumbnail_height, 1); 20 | assert.true(data.url.length > 0); 21 | }); 22 | 23 | test("getMetadata - other file", async function (assert) { 24 | const file = new File(["test"], "test.txt", { type: "text/plain" }); 25 | const data = await getMetadata(file, SITE_SETTINGS); 26 | assert.strictEqual(data.original_filename, "test.txt"); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /translator.yml: -------------------------------------------------------------------------------- 1 | # Configuration file for discourse-translator-bot 2 | 3 | files: 4 | - source_path: config/locales/client.en.yml 5 | destination_path: client.yml 6 | - source_path: config/locales/server.en.yml 7 | destination_path: server.yml 8 | --------------------------------------------------------------------------------