Compare commits

...

99 Commits

Author SHA1 Message Date
Vincent Breitmoser
facd64f236 about: add faq entry about partial searches 2019-12-16 13:33:43 +01:00
Vincent Breitmoser
351652d845 tx pull, new language: turkish 2019-12-11 10:37:20 +01:00
Vincent Breitmoser
d15f0926ed hkp: return correct creation time
The creation time reported in the hkp "index" response should be the
primary key's creation timestamp. We returned the creation time of the
"primary key signature" (i.e.  newest user id binding signature) before.

see https://tools.ietf.org/html/draft-shaw-openpgp-hkp-00#section-5.2
2019-12-10 17:23:08 +01:00
Vincent Breitmoser
230f69bc6f i18n: tx pull 2019-11-28 00:06:43 +01:00
Vincent Breitmoser
dae530d090 fix and add my name to authors list in Cargo.toml 2019-11-28 00:06:43 +01:00
Vincent Breitmoser
70ab7850d6 search: improve detection of short key id queries 2019-11-27 12:17:49 +01:00
Vincent Breitmoser
3a9965ccaa about: fix atom link in layout meta tags 2019-11-20 16:59:41 +01:00
Vincent Breitmoser
ec9e5f4d87 about: mention empty expiration field in API docs
Link to discussion in issue #134
2019-11-19 13:11:50 +01:00
Vincent Breitmoser
6b311dfa8b Merge remote-tracking branch 'origin/master' into i18n-updates-1 2019-11-19 12:38:31 +01:00
Vincent Breitmoser
3cf2f08784 i18n: tx pull 2019-11-19 12:28:35 +01:00
Vincent Breitmoser
21a33d81f0 i18n: fix tests (again) 2019-11-18 15:40:03 +01:00
Vincent Breitmoser
956afbf47a i18n: tx pull again and fix unit tests 2019-11-18 11:26:45 +01:00
Vincent Breitmoser
54382554d5 i18n: add 100k news entry to translation sources 2019-11-17 23:43:22 +01:00
Vincent Breitmoser
48e69e5899 i18n: tx pull 2019-11-17 23:39:07 +01:00
Vincent Breitmoser
7ff7129b31 i18n: add Polish 2019-11-17 23:39:07 +01:00
Vincent Breitmoser
a8d95fae83 i18n: include "latin extended" glyph set in roboto font 2019-11-17 23:39:07 +01:00
Vincent Breitmoser
ce846230aa about: use alice sample key for "no user id" faq answer 2019-11-17 23:39:06 +01:00
Vincent Breitmoser
9309d1c700 i18n: git ignore generated localized template files 2019-11-17 23:39:06 +01:00
Vincent Breitmoser
2127b3723a i18n: tx pull 2019-11-17 23:39:06 +01:00
Vincent Breitmoser
9f685d4b89 i18n: fix some more "email" strings 2019-11-17 23:38:31 +01:00
Vincent Breitmoser
e0fea8013b i18n: ignore .po~ files 2019-11-17 23:38:31 +01:00
Vincent Breitmoser
10b177be8e i18n: fix up english translations from untranslated templates, too 2019-11-17 23:38:31 +01:00
Vincent Breitmoser
555c895065 i18n: more AO feedback 2019-11-17 23:38:30 +01:00
Vincent Breitmoser
9f90a527ad i18n: add french translation 2019-11-17 23:38:30 +01:00
Vincent Breitmoser
9c0e7d6ffe i18n: more feedback from AO 2019-11-17 23:37:31 +01:00
Vincent Breitmoser
2556073a63 i18n: tx pull 2019-11-17 23:37:31 +01:00
Vincent Breitmoser
130a13842d about: news post about reaching 100k verified addresses! 2019-11-13 01:00:36 +01:00
Vincent Breitmoser
609da73320 about: show yearly+monthly stats, instead of monthly+weekly 2019-11-11 13:00:46 +01:00
Vincent Breitmoser
b067c42f02 db: add unix timestamp to db log lines 2019-11-08 00:25:23 +01:00
Vincent Breitmoser
c624f2a5cf db: implement simple logging 2019-11-08 00:12:39 +01:00
Vincent Breitmoser
1a319fc2b9 db: fix compiler warnings 2019-11-08 00:12:37 +01:00
Vincent Breitmoser
8cf7adbb6c cargo update, and drop patched runtime-fmt 2019-11-03 00:53:53 +01:00
Vincent Breitmoser
435f8d4743 i18n: minuscule improvements 2019-11-02 23:55:56 +01:00
Vincent Breitmoser
9e62a001ab i18n: don't translate welcome message, we don't get a locale anyways 2019-11-02 23:55:55 +01:00
Vincent Breitmoser
58aae5f8f8 i18n: small technical improvements to email translations 2019-11-02 23:55:53 +01:00
Vincent Breitmoser
7a8574c133 i18n: redo email template signature 2019-11-02 23:55:52 +01:00
Vincent Breitmoser
4934d15165 i18n: streamline upload errors 2019-11-02 23:55:51 +01:00
Vincent Breitmoser
f38f5ced40 i18n: make some more errors translatable 2019-11-02 23:55:51 +01:00
Vincent Breitmoser
0f598323c8 i18n: add error message on expired upload session 2019-11-02 23:55:51 +01:00
Vincent Breitmoser
ccb3a9fcbc upload: fix template, show errors again 2019-11-02 23:55:50 +01:00
Vincent Breitmoser
c57c00b390 fixup! i18n: incorporate feedback from AO 2019-11-02 23:55:50 +01:00
Vincent Breitmoser
b91750372f i18n: e-mail -> email, in about files 2019-11-02 23:55:50 +01:00
Vincent Breitmoser
178e6544a5 i18n: pull from transifex 2019-11-02 23:55:50 +01:00
Vincent Breitmoser
041385ae4e i18n: incorporate feedback from AO 2019-11-02 23:55:50 +01:00
Vincent Breitmoser
ef5a664827 i18n: fix module order for include_i18n! macro 2019-10-05 14:46:00 +02:00
Vincent Breitmoser
07e793b023 cargo: use runtime-fmt with fix for nightly rustc 2019-10-05 14:46:00 +02:00
Vincent Breitmoser
ff66332ea7 news: add atom feed 2019-10-05 14:46:00 +02:00
Vincent Breitmoser
695e4af7e9 i18n: pull from transifex 2019-10-05 14:46:00 +02:00
Vincent Breitmoser
c50b91e6cf i18n: add script for post-processing translated overridden templates 2019-10-04 13:59:01 +02:00
Vincent Breitmoser
0225e63b54 cleanup: remove "extern crate" lines 2019-10-03 18:18:48 +02:00
Vincent Breitmoser
5704ac4434 i18n: encode subject in mails, if necessary 2019-10-03 18:18:47 +02:00
Vincent Breitmoser
cdc1afb6d1 i18n: prepare translation of about pages 2019-10-03 18:18:46 +02:00
Vincent Breitmoser
6236d3bf40 i18n: add template override mechanism 2019-10-03 18:18:45 +02:00
Vincent Breitmoser
069d332123 cargo update 2019-10-03 18:18:45 +02:00
Vincent Breitmoser
3355c7e5d4 upload: fix template 2019-10-03 18:18:44 +02:00
Vincent Breitmoser
1f2c4047d6 i18n: more dynamic strings 2019-10-03 18:18:44 +02:00
Vincent Breitmoser
9152e2d3dd i18n: translate more dynamic messages 2019-10-03 18:18:43 +02:00
Vincent Breitmoser
82f5450eb5 i18n: add German (de) 2019-10-03 18:18:43 +02:00
Vincent Breitmoser
471e521484 i18n: add transifex translation data 2019-10-03 18:18:42 +02:00
Vincent Breitmoser
c36161650b i18n: prepare mail templates for localization, too 2019-10-03 18:18:42 +02:00
Vincent Breitmoser
0fced131c5 i18n: working i18n based on gettext 2019-10-03 18:18:42 +02:00
Vincent Breitmoser
331ef566ef i18n: translate mail subjects 2019-10-03 18:18:42 +02:00
Vincent Breitmoser
77e9c13b5b i18n: localize mail templates 2019-10-03 18:18:41 +02:00
Vincent Breitmoser
77c90fc8fb i18n: first steps 2019-10-03 18:18:41 +02:00
Vincent Breitmoser
6e0ab047ee vks: slightly improve wording for quick upload 2019-10-03 18:18:39 +02:00
Vincent Breitmoser
ecdf1001f4 upload: improve error message for double verifications
Keep a rate limiter token around, and tell the user that a link was
already clicked recently if that is the case.
2019-09-26 23:10:13 +02:00
Vincent Breitmoser
02321777b9 upload: simplify automatic POST request 2019-09-26 23:08:54 +02:00
Daniel Silverstone
6df212f087 upload: Require POST for token verification
In order to mitigate against MUAs previewing URLs, move the token
verification flow to a POST handler, and add a new GET handler
which returns a form requiring the user to click an additional time
in order to verify their address.

The returned form also carries some JavaScript which will attempt to
do this for the user, meaning the experience for the user should be
almost exactly as before, while mitigating MUA previews.

Closes: #53

Signed-off-by: Daniel Silverstone <dsilvers@digital-scurf.org>
2019-09-26 21:42:48 +02:00
Daniel Silverstone
6b7cbbe1c1 site.css: Add nowrap to span.email to reduce ugly rendering
Signed-off-by: Daniel Silverstone <dsilvers@digital-scurf.org>
2019-09-26 21:02:11 +02:00
Vincent Breitmoser
ce7356cc72 about: write "three months later" news entry 2019-09-12 21:12:59 +02:00
Vincent Breitmoser
52e863281d nginx: fix hkp routes
Changes handling of hkp routes:
- uri decode the search paramter before passing to /pks/internal/$op/$query
- strip <> surrounded text in lua, instead of an nginx rule
2019-09-12 12:02:29 +00:00
8805cb7790 README: correct link 2019-09-06 19:21:32 +02:00
Vincent Breitmoser
2c48d151b1 about: status update about gpg tools 2019-09-05 20:12:50 +02:00
Vincent Breitmoser
c180f3a905 fix redundant import warning 2019-09-02 23:12:33 +02:00
Vincent Breitmoser
6126436bca cargo update 2019-09-02 23:09:54 +02:00
Vincent Breitmoser
41ea7111d9 update to rust 2018 edition 2019-09-02 23:09:52 +02:00
Vincent Breitmoser
2eaec0b7dd externalize all js 2019-08-13 17:25:17 +02:00
Vincent Breitmoser
2440d37485 prometheus: prefix metrics with hagrid_ 2019-07-20 20:06:35 +02:00
Vincent Breitmoser
eec7765b54 prometheus: aggregate similar stats using labels 2019-07-20 17:00:14 +02:00
Vincent Breitmoser
88aff3d5c4 anon_utils: use lazy static hashset for known domains 2019-07-20 00:13:27 +02:00
Vincent Breitmoser
a9a6c51fac prometheus: introduce a couple simple prometheus stats 2019-07-20 00:13:23 +02:00
Vincent Breitmoser
8a74b74e32 hkp: also apply suggestion to txt version of welcome mail 2019-07-19 17:32:00 +02:00
Vincent Breitmoser
dc4fc6b33f Apply suggestion to dist/templates/email/welcome-html.hbs 2019-07-17 12:13:15 +00:00
Vincent Breitmoser
9a71103fe7 hkp: welcome email on upload of previously unknown key 2019-07-16 10:09:57 +02:00
Vincent Breitmoser
f38a530ae9 vks_web: split up request and response processing 2019-07-16 10:09:55 +02:00
Vincent Breitmoser
f64dcdb72a about: add Troubleshooting to GnuPG usage 2019-07-15 21:13:49 +02:00
Vincent Breitmoser
a8d69c0cbf about: small fixes in usage page 2019-07-15 12:35:03 +02:00
Vincent Breitmoser
8e079eb9fd about: update usage page 2019-07-15 12:09:49 +02:00
Vincent Breitmoser
389b85b43a about: update documentation on hkp limits 2019-07-12 18:11:22 +02:00
Vincent Breitmoser
45e667ca55 cargo update 2019-07-12 12:52:06 +02:00
Vincent Breitmoser
4dcdff15c2 nginx: further improve rewrite rules (get rid of most ifs) 2019-07-12 12:27:31 +02:00
Vincent Breitmoser
9a551dc16e errors: use 429 for rate limiting instead of 503
GnuPG really doesn't like 503 errors, see https://dev.gnupg.org/T4600
2019-07-12 12:27:31 +02:00
Vincent Breitmoser
a69695dc2c fix pks index after rate limiting 2019-07-12 12:27:30 +02:00
Vincent Breitmoser
5248b1d0b9 nginx: add some rate limiting and improve error handling 2019-07-12 12:27:30 +02:00
Vincent Breitmoser
3b596d4955 vks_api: fix returned errors to be plain 2019-07-12 12:27:30 +02:00
Vincent Breitmoser
dd6c69f11d errors: add hagrid templated error handler 2019-07-12 12:27:28 +02:00
Jonathan Frederick
9f4b82a9b1 Fix 2 warnings 2019-07-09 16:59:03 -07:00
Justus Winter
d628acdf93 Bump Sequoia to 0.9.
- Sequoia now ignores any unhashed subpackets when comparing
    signatures, closing a DoS vector.  Previously, one could create
    any number of valid signatures from a single valid signature that
    were considered distinct by mutating the unhashed subpacket area.

  - Sequoia now handles malformed UserIDs of the form "$addr <$addr>".
    Adjust database::test::test_bad_uids accordingly.

  - Update src/dump.rs from Sequoia.

  - Sequoia now ignores any Unicode codepoint considered whitespace
    when parsing Fingerprints and KeyIDs.  Fixes #122.
2019-07-08 19:16:39 +02:00
Justus Winter
71d38ae865 Revert "web: ignore whitespace in searched fingerprints"
- ASCII whitespace, among them the space (0x20), was always ignored
    by Sequoia, therefore the change did not change what search terms
    were accepted.

  - This reverts commit ab3f4006dd.
2019-07-08 17:10:24 +02:00
121 changed files with 8057 additions and 1493 deletions

2
.gitignore vendored
View File

@@ -5,3 +5,5 @@
**/.*.swp
**/.*.swo
target
*.po~
/dist/templates/localized

View File

@@ -10,5 +10,6 @@ build:binary:
- 'echo "deb http://deb.debian.org/debian testing main" > /etc/apt/sources.list.d/testing-nettle.list'
- 'echo "Package: *\nPin: release a=testing\nPin-Priority: 150" > /etc/apt/preferences.d/limit_testing'
- apt update -qy
- apt install -qy libclang-dev build-essential pkg-config clang nettle-dev/testing
- apt install -qy libclang-dev build-essential pkg-config clang nettle-dev/testing gettext zsh
- ./make-translated-templates
- RUST_BACKTRACE=full cargo test --all

59
.tx/config Normal file
View File

@@ -0,0 +1,59 @@
[main]
host = https://www.transifex.com
[hagrid.hagrid]
minimum_perc = 100
source_file = po/hagrid/hagrid.pot
source_lang = en
file_filter = po/hagrid/<lang>.po
trans.zh-Hans = po/hagrid/zh_Hans.po
type = PO
[hagrid.about-about]
minimum_perc = 100
source_file = templates-untranslated/about/about.html.hbs
file_filter = templates-translated/<lang>/about/about.html.hbs
trans.zh-Hans = templates-translated/zh_Hans/about/about.html.hbs
source_lang = en
type = HTML
[hagrid.about-faq]
minimum_perc = 100
source_file = templates-untranslated/about/faq.html.hbs
file_filter = templates-translated/<lang>/about/faq.html.hbs
trans.zh-Hans = templates-translated/zh_Hans/about/faq.html.hbs
source_lang = en
type = HTML
[hagrid.about-news]
minimum_perc = 100
source_file = templates-untranslated/about/news.html.hbs
file_filter = templates-translated/<lang>/about/news.html.hbs
trans.zh-Hans = templates-translated/zh_Hans/about/news.html.hbs
source_lang = en
type = HTML
[hagrid.about-privacy]
minimum_perc = 100
source_file = templates-untranslated/about/privacy.html.hbs
file_filter = templates-translated/<lang>/about/privacy.html.hbs
trans.zh-Hans = templates-translated/zh_Hans/about/privacy.html.hbs
source_lang = en
type = HTML
[hagrid.about-stats]
minimum_perc = 100
source_file = templates-untranslated/about/stats.html.hbs
file_filter = templates-translated/<lang>/about/stats.html.hbs
trans.zh-Hans = templates-translated/zh_Hans/about/stats.html.hbs
source_lang = en
type = HTML
[hagrid.about-usage]
minimum_perc = 100
source_file = templates-untranslated/about/usage.html.hbs
file_filter = templates-translated/<lang>/about/usage.html.hbs
trans.zh-Hans = templates-translated/zh_Hans/about/usage.html.hbs
source_lang = en
type = HTML

1263
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,10 @@
[package]
name = "hagrid"
version = "0.1.0"
authors = ["Kai Michaelis <kai@sequoia-pgp.org>"]
authors = ["Vincent Breitmoser <look@my.amazin.horse>", "Kai Michaelis <kai@sequoia-pgp.org>"]
build = "build.rs"
default-run = "hagrid"
edition = "2018"
[workspace]
members = [
@@ -16,9 +17,8 @@ hagrid-database = { path = "database" }
failure = "0.1.5"
rocket = "0"
rocket_codegen = "0"
sequoia-openpgp = { version = "0.8", default-features = false }
sequoia-openpgp = { version = "0.9", default-features = false }
multipart = "0"
log = "0"
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"
@@ -32,6 +32,14 @@ num_cpus = "1.0"
ring = "0.13"
base64 = "0.10"
uuid = "0.7"
rocket_prometheus = "0.2"
lazy_static = "1.3.0"
rocket_i18n = "0.4"
gettext-macros = "0.5"
runtime-fmt = "0.4"
gettext = "0.4"
glob = "0.3"
rfc2047 = "0.1"
[dependencies.lettre]
version = "0.9"

View File

@@ -4,7 +4,7 @@ Hagrid
Hagrid is a verifying OpenPGP key server.
You can find general instructions and an API documentation at the running
instance at [https://keys.openpgp.org](keys.openpgp.org).
instance at [https://keys.openpgp.org](https://keys.openpgp.org).
License
-------

View File

@@ -16,6 +16,8 @@ token_dir = "state/tokens"
tmp_dir = "state/tmp"
mail_rate_limit = 60
maintenance_file = "state/maintenance"
enable_prometheus = false
email_template_dir = "dist/email-templates"
[staging]
base-URI = "https://keys.openpgp.org"
@@ -31,6 +33,8 @@ token_dir = "tokens"
tmp_dir = "tmp"
mail_rate_limit = 60
maintenance_file = "maintenance"
enable_prometheus = false
email_template_dir = "email-templates"
[production]
base-URI = "https://keys.openpgp.org"
@@ -47,3 +51,5 @@ token_dir = "tokens"
tmp_dir = "tmp"
mail_rate_limit = 3600
maintenance_file = "maintenance"
enable_prometheus = false
email_template_dir = "email-templates"

View File

@@ -5,7 +5,7 @@ authors = ["Kai Michaelis <kai@sequoia-pgp.org>"]
[dependencies]
failure = "0.1.5"
sequoia-openpgp = { version = "0.8", default-features = false }
sequoia-openpgp = { version = "0.9", default-features = false }
multipart = "0"
log = "0"
rand = "0.6"
@@ -21,6 +21,7 @@ pathdiff = "0.1"
idna = "0.1"
fs2 = "0.4"
walkdir = "2.2"
chrono = "0.4"
[lib]
name = "hagrid_database"

View File

@@ -1,6 +1,6 @@
use std::collections::HashMap;
use std::convert::TryFrom;
use std::fs::{create_dir_all, read_link, remove_file, rename, set_permissions, Permissions};
use std::fs::{OpenOptions, File, create_dir_all, read_link, remove_file, rename, set_permissions, Permissions};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::os::unix::fs::PermissionsExt;
@@ -8,6 +8,7 @@ use std::os::unix::fs::PermissionsExt;
use tempfile;
use url;
use pathdiff::diff_paths;
use std::time::SystemTime;
//use sequoia_openpgp::armor::{Writer, Kind};
@@ -28,6 +29,7 @@ pub struct Filesystem {
keys_dir_full: PathBuf,
keys_dir_quarantined: PathBuf,
keys_dir_published: PathBuf,
keys_dir_log: PathBuf,
links_dir_by_fingerprint: PathBuf,
links_dir_by_keyid: PathBuf,
@@ -78,10 +80,12 @@ impl Filesystem {
let keys_external_dir: PathBuf = keys_external_dir.into();
let keys_dir_full = keys_internal_dir.join("full");
let keys_dir_quarantined = keys_internal_dir.join("quarantined");
let keys_dir_log = keys_internal_dir.join("log");
let keys_dir_published = keys_external_dir.join("pub");
create_dir_all(&keys_dir_full)?;
create_dir_all(&keys_dir_quarantined)?;
create_dir_all(&keys_dir_published)?;
create_dir_all(&keys_dir_log)?;
let links_dir = keys_external_dir.join("links");
let links_dir_by_keyid = links_dir.join("by-keyid");
@@ -103,6 +107,7 @@ impl Filesystem {
keys_dir_full,
keys_dir_published,
keys_dir_quarantined,
keys_dir_log,
links_dir_by_keyid,
links_dir_by_fingerprint,
@@ -199,6 +204,14 @@ impl Filesystem {
}
}
fn open_logfile(&self, file_name: &str) -> Result<File> {
let file_path = self.keys_dir_log.join(file_name);
Ok(OpenOptions::new()
.create(true)
.append(true)
.open(file_path)?)
}
fn perform_checks(
&self,
checks_dir: &Path,
@@ -278,6 +291,19 @@ impl Database for Filesystem {
Ok(tempfile)
}
fn write_log_append(&self, filename: &str, fpr_primary: &Fingerprint) -> Result<()> {
let timestamp = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs();
let fingerprint_line = format!("{:010} {}\n", timestamp, fpr_primary.to_string());
self.open_logfile(filename)?
.write_all(fingerprint_line.as_bytes())?;
Ok(())
}
fn move_tmp_to_full(&self, file: NamedTempFile, fpr: &Fingerprint) -> Result<()> {
if self.dry_run {
return Ok(());
@@ -478,7 +504,6 @@ impl Database for Filesystem {
/// Note that this operation may take a long time, and is
/// generally only useful for testing.
fn check_consistency(&self) -> Result<()> {
use std::collections::HashMap;
use failure::format_err;
// A cache of all TPKs, for quick lookups.
@@ -629,10 +654,17 @@ mod tests {
let _ = Filesystem::new_from_base(tmpdir.path()).unwrap();
}
#[test]
fn new() {
fn open_db() -> (TempDir, Filesystem, PathBuf) {
let tmpdir = TempDir::new().unwrap();
let db = Filesystem::new_from_base(tmpdir.path()).unwrap();
let log_path = db.keys_dir_log.join(db.get_current_log_filename());
(tmpdir, db, log_path)
}
#[test]
fn new() {
let (_tmp_dir, db, _log_path) = open_db();
let k1 = TPKBuilder::new().add_userid("a@invalid.example.org")
.generate().unwrap().0;
let k2 = TPKBuilder::new().add_userid("b@invalid.example.org")
@@ -650,125 +682,99 @@ mod tests {
#[test]
fn uid_verification() {
let tmpdir = TempDir::new().unwrap();
let mut db = Filesystem::new_from_base(tmpdir.path()).unwrap();
test::test_uid_verification(&mut db);
let (_tmp_dir, mut db, log_path) = open_db();
test::test_uid_verification(&mut db, &log_path);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn uid_deletion() {
let tmpdir = TempDir::new().unwrap();
let mut db = Filesystem::new_from_base(tmpdir.path()).unwrap();
test::test_uid_deletion(&mut db);
let (_tmp_dir, mut db, log_path) = open_db();
test::test_uid_deletion(&mut db, &log_path);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn subkey_lookup() {
let tmpdir = TempDir::new().unwrap();
let mut db = Filesystem::new_from_base(tmpdir.path()).unwrap();
test::test_subkey_lookup(&mut db);
let (_tmp_dir, mut db, log_path) = open_db();
test::test_subkey_lookup(&mut db, &log_path);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn kid_lookup() {
let tmpdir = TempDir::new().unwrap();
let mut db = Filesystem::new_from_base(tmpdir.path()).unwrap();
test::test_kid_lookup(&mut db);
let (_tmp_dir, mut db, log_path) = open_db();
test::test_kid_lookup(&mut db, &log_path);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn upload_revoked_tpk() {
let tmpdir = TempDir::new().unwrap();
let mut db = Filesystem::new_from_base(tmpdir.path()).unwrap();
test::test_upload_revoked_tpk(&mut db);
let (_tmp_dir, mut db, log_path) = open_db();
test::test_upload_revoked_tpk(&mut db, &log_path);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn uid_revocation() {
let tmpdir = TempDir::new().unwrap();
let mut db = Filesystem::new_from_base(tmpdir.path()).unwrap();
test::test_uid_revocation(&mut db);
let (_tmp_dir, mut db, log_path) = open_db();
test::test_uid_revocation(&mut db, &log_path);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn regenerate() {
let tmpdir = TempDir::new().unwrap();
let mut db = Filesystem::new_from_base(tmpdir.path()).unwrap();
test::test_regenerate(&mut db);
let (_tmp_dir, mut db, log_path) = open_db();
test::test_regenerate(&mut db, &log_path);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn key_reupload() {
let tmpdir = TempDir::new().unwrap();
let mut db = Filesystem::new_from_base(tmpdir.path()).unwrap();
test::test_reupload(&mut db);
let (_tmp_dir, mut db, log_path) = open_db();
test::test_reupload(&mut db, &log_path);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn uid_replacement() {
let tmpdir = TempDir::new().unwrap();
let mut db = Filesystem::new_from_base(tmpdir.path()).unwrap();
test::test_uid_replacement(&mut db);
let (_tmp_dir, mut db, log_path) = open_db();
test::test_uid_replacement(&mut db, &log_path);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn uid_unlinking() {
let tmpdir = TempDir::new().unwrap();
let mut db = Filesystem::new_from_base(tmpdir.path()).unwrap();
test::test_unlink_uid(&mut db);
let (_tmp_dir, mut db, log_path) = open_db();
test::test_unlink_uid(&mut db, &log_path);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn same_email_1() {
let tmpdir = TempDir::new().unwrap();
let mut db = Filesystem::new_from_base(tmpdir.path()).unwrap();
test::test_same_email_1(&mut db);
let (_tmp_dir, mut db, log_path) = open_db();
test::test_same_email_1(&mut db, &log_path);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn same_email_2() {
let tmpdir = TempDir::new().unwrap();
let mut db = Filesystem::new_from_base(tmpdir.path()).unwrap();
test::test_same_email_2(&mut db);
let (_tmp_dir, mut db, log_path) = open_db();
test::test_same_email_2(&mut db, &log_path);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn no_selfsig() {
let tmpdir = TempDir::new().unwrap();
let mut db = Filesystem::new_from_base(tmpdir.path()).unwrap();
test::test_no_selfsig(&mut db);
let (_tmp_dir, mut db, log_path) = open_db();
test::test_no_selfsig(&mut db, &log_path);
db.check_consistency().expect("inconsistent database");
}
#[test]
fn bad_uids() {
let tmpdir = TempDir::new().unwrap();
let mut db = Filesystem::new_from_base(tmpdir.path()).unwrap();
test::test_bad_uids(&mut db);
let (_tmp_dir, mut db, log_path) = open_db();
test::test_bad_uids(&mut db, &log_path);
db.check_consistency().expect("inconsistent database");
}

View File

@@ -1,11 +1,12 @@
#![feature(proc_macro_hygiene, plugin, decl_macro)]
#![recursion_limit = "1024"]
#![feature(try_from)]
use std::convert::TryFrom;
use std::path::PathBuf;
use std::str::FromStr;
use chrono::prelude::Utc;
extern crate failure;
use failure::Error;
use failure::Fallible as Result;
@@ -21,6 +22,7 @@ extern crate time;
extern crate url;
extern crate hex;
extern crate walkdir;
extern crate chrono;
use tempfile::NamedTempFile;
@@ -63,14 +65,15 @@ impl FromStr for Query {
fn from_str(term: &str) -> Result<Self> {
use self::Query::*;
if term.starts_with("0x") && term.len() < 16 && !term.contains('@') {
let looks_like_short_key_id = !term.contains('@') &&
(term.starts_with("0x") && term.len() < 16 || term.len() == 8);
if looks_like_short_key_id {
return Err(failure::err_msg(
"Search by Short Key ID is not supported, sorry!"));
}
let term_nospace = term.replace(" ", "");
if let Ok(fp) = Fingerprint::from_str(&term_nospace) {
if let Ok(fp) = Fingerprint::from_str(term) {
Ok(ByFingerprint(fp))
} else if let Ok(keyid) = KeyID::from_str(&term_nospace) {
} else if let Ok(keyid) = KeyID::from_str(term) {
Ok(ByKeyID(keyid))
} else if let Ok(email) = Email::from_str(term) {
Ok(ByEmail(email))
@@ -216,9 +219,11 @@ pub trait Database: Sync + Send {
return Err(failure::err_msg("Not a well-formed key!"));
}
let published_uids: Vec<UserID> = self
let published_tpk_old = self
.by_fpr(&fpr_primary)
.and_then(|bytes| TPK::from_bytes(bytes.as_ref()).ok())
.and_then(|bytes| TPK::from_bytes(bytes.as_ref()).ok());
let published_uids: Vec<UserID> = published_tpk_old
.as_ref()
.map(|tpk| tpk.userids()
.map(|binding| binding.userid().clone())
.collect()
@@ -310,6 +315,13 @@ pub trait Database: Sync + Send {
self.move_tmp_to_full(full_tpk_tmp, &fpr_primary)?;
self.move_tmp_to_published(published_tpk_tmp, &fpr_primary)?;
let published_tpk_changed = published_tpk_old
.map(|tpk| tpk != published_tpk_clean)
.unwrap_or(true);
if published_tpk_changed {
self.update_write_log(&fpr_primary);
}
for fpr in fpr_not_linked {
if let Err(e) = self.link_fpr(&fpr, &fpr_primary) {
info!("Error ensuring symlink! {} {} {:?}",
@@ -331,6 +343,18 @@ pub trait Database: Sync + Send {
}
}
fn update_write_log(&self, fpr_primary: &Fingerprint) {
let log_name = self.get_current_log_filename();
println!("{}", log_name);
if let Err(e) = self.write_log_append(&log_name, fpr_primary) {
error!("Error writing to log! {} {} {}", &log_name, &fpr_primary, e);
}
}
fn get_current_log_filename(&self) -> String {
Utc::now().format("%Y-%m-%d").to_string()
}
fn get_tpk_status(&self, fpr_primary: &Fingerprint, known_addresses: &[Email]) -> Result<TpkStatus> {
let tpk_full = self.by_fpr_full(&fpr_primary)
.ok_or_else(|| failure::err_msg("Key not in database!"))
@@ -435,6 +459,7 @@ pub trait Database: Sync + Send {
let published_tpk_tmp = self.write_to_temp(&tpk_to_string(&published_tpk_clean)?)?;
self.move_tmp_to_published(published_tpk_tmp, &fpr_primary)?;
self.update_write_log(&fpr_primary);
if let Err(e) = self.link_email(&email_new, &fpr_primary) {
info!("Error ensuring email symlink! {} -> {} {:?}",
@@ -518,6 +543,7 @@ pub trait Database: Sync + Send {
let published_tpk_tmp = self.write_to_temp(&tpk_to_string(&published_tpk_clean)?)?;
self.move_tmp_to_published(published_tpk_tmp, &fpr_primary)?;
self.update_write_log(&fpr_primary);
for unpublished_email in unpublished_emails {
if let Err(e) = self.unlink_email(&unpublished_email, &fpr_primary) {
@@ -602,6 +628,7 @@ pub trait Database: Sync + Send {
fn move_tmp_to_full(&self, content: NamedTempFile, fpr: &Fingerprint) -> Result<()>;
fn move_tmp_to_published(&self, content: NamedTempFile, fpr: &Fingerprint) -> Result<()>;
fn write_to_quarantine(&self, fpr: &Fingerprint, content: &[u8]) -> Result<()>;
fn write_log_append(&self, filename: &str, fpr_primary: &Fingerprint) -> Result<()>;
fn check_consistency(&self) -> Result<()>;
}

View File

@@ -98,6 +98,6 @@ mod tests {
#[test]
fn flock_nonexistent() {
FlockMutexGuard::lock("nonexistent").is_err();
assert!(FlockMutexGuard::lock("nonexistent").is_err());
}
}

View File

@@ -19,18 +19,20 @@ use std::str::FromStr;
use Database;
use Query;
use openpgp::tpk::{TPKBuilder, UserIDBinding};
use openpgp::tpk::TPKBuilder;
use openpgp::{
constants::ReasonForRevocation, constants::SignatureType, packet::UserID,
parse::Parse, Packet, PacketPile, RevocationStatus, TPK,
packet::KeyFlags
};
use types::{Email, Fingerprint, KeyID};
use std::path::Path;
use std::fs;
use TpkStatus;
use EmailAddressStatus;
pub fn test_uid_verification<D: Database>(db: &mut D) {
pub fn test_uid_verification(db: &mut impl Database, log_path: &Path) {
let str_uid1 = "Test A <test_a@example.com>";
let str_uid2 = "Test B <test_b@example.com>";
let tpk = TPKBuilder::new()
@@ -43,10 +45,11 @@ pub fn test_uid_verification<D: Database>(db: &mut D) {
let uid2 = UserID::from(str_uid2);
let email1 = Email::from_str(str_uid1).unwrap();
let email2 = Email::from_str(str_uid2).unwrap();
let fpr = Fingerprint::try_from(tpk.fingerprint()).unwrap();
// upload key
let tpk_status = db.merge(tpk.clone()).unwrap().into_tpk_status();
let fpr = Fingerprint::try_from(tpk.fingerprint()).unwrap();
check_log_entry(log_path, &fpr);
assert_eq!(TpkStatus {
is_revoked: false,
@@ -153,6 +156,7 @@ pub fn test_uid_verification<D: Database>(db: &mut D) {
}
let tpk_status = db.merge(tpk.clone()).unwrap().into_tpk_status();
check_log_entry(log_path, &fpr);
assert_eq!(TpkStatus {
is_revoked: false,
email_status: vec!(
@@ -262,7 +266,7 @@ pub fn test_uid_verification<D: Database>(db: &mut D) {
}*/
}
pub fn test_regenerate<D: Database>(db: &mut D) {
pub fn test_regenerate(db: &mut impl Database, log_path: &Path) {
let str_uid1 = "Test A <test_a@example.com>";
let tpk = TPKBuilder::new()
.add_userid(str_uid1)
@@ -284,6 +288,7 @@ pub fn test_regenerate<D: Database>(db: &mut D) {
// upload key
db.merge(tpk).unwrap().into_tpk_status();
check_log_entry(log_path, &fpr);
db.regenerate_links(&fpr).unwrap();
assert!(db.by_email(&email1).is_none());
@@ -309,7 +314,7 @@ pub fn test_regenerate<D: Database>(db: &mut D) {
assert!(db.check_consistency().is_ok());
}
pub fn test_reupload<D: Database>(db: &mut D) {
pub fn test_reupload(db: &mut impl Database, log_path: &Path) {
let str_uid1 = "Test A <test_a@example.com>";
let str_uid2 = "Test B <test_b@example.com>";
let tpk = TPKBuilder::new()
@@ -324,6 +329,7 @@ pub fn test_reupload<D: Database>(db: &mut D) {
// upload key
db.merge(tpk.clone()).unwrap().into_tpk_status();
check_log_entry(log_path, &fpr);
// verify 1st uid
db.set_email_published(&fpr, &email1).unwrap();
@@ -343,7 +349,7 @@ pub fn test_reupload<D: Database>(db: &mut D) {
assert!(db.by_email(&email2).is_none() ^ db.by_email(&email1).is_none());
}
pub fn test_uid_replacement<D: Database>(db: &mut D) {
pub fn test_uid_replacement(db: &mut impl Database, log_path: &Path) {
let str_uid1 = "Test A <test_a@example.com>";
let tpk1 = TPKBuilder::new().add_userid(str_uid1).generate().unwrap().0;
let fpr1 = Fingerprint::try_from(tpk1.fingerprint()).unwrap();
@@ -358,7 +364,9 @@ pub fn test_uid_replacement<D: Database>(db: &mut D) {
// upload both keys
db.merge(tpk1).unwrap().into_tpk_status();
check_log_entry(log_path, &fpr1);
db.merge(tpk2).unwrap().into_tpk_status();
check_log_entry(log_path, &fpr2);
// verify 1st uid
db.set_email_published(&fpr1, &email1).unwrap();
@@ -383,7 +391,7 @@ pub fn test_uid_replacement<D: Database>(db: &mut D) {
.userids().len(), 1);
}
pub fn test_uid_deletion<D: Database>(db: &mut D) {
pub fn test_uid_deletion(db: &mut impl Database, log_path: &Path) {
let str_uid1 = "Test A <test_a@example.com>";
let str_uid2 = "Test B <test_b@example.com>";
let tpk = TPKBuilder::new()
@@ -401,6 +409,7 @@ pub fn test_uid_deletion<D: Database>(db: &mut D) {
// upload key and verify uids
let tpk_status = db.merge(tpk).unwrap().into_tpk_status();
check_log_entry(log_path, &fpr);
assert_eq!(TpkStatus {
is_revoked: false,
email_status: vec!(
@@ -442,7 +451,7 @@ pub fn test_uid_deletion<D: Database>(db: &mut D) {
assert_eq!(tpk.subkeys().count(), n_subkeys);
}
pub fn test_subkey_lookup<D: Database>(db: &mut D) {
pub fn test_subkey_lookup(db: &mut impl Database, _log_path: &Path) {
let tpk = TPKBuilder::new()
.add_userid("Testy <test@example.com>")
.add_signing_subkey()
@@ -454,8 +463,6 @@ pub fn test_subkey_lookup<D: Database>(db: &mut D) {
// upload key
let _ = db.merge(tpk.clone()).unwrap().into_tpk_status();
// upload key
let _ = db.merge(tpk.clone()).unwrap().into_tpk_status();
let fpr_primray = Fingerprint::try_from(tpk.fingerprint()).unwrap();
let fpr_sign: Fingerprint = tpk.keys_all()
.signing_capable()
@@ -474,7 +481,7 @@ pub fn test_subkey_lookup<D: Database>(db: &mut D) {
assert_eq!(raw1, raw2);
}
pub fn test_kid_lookup<D: Database>(db: &mut D) {
pub fn test_kid_lookup(db: &mut impl Database, _log_path: &Path) {
let tpk = TPKBuilder::new()
.add_userid("Testy <test@example.com>")
.add_signing_subkey()
@@ -503,7 +510,7 @@ pub fn test_kid_lookup<D: Database>(db: &mut D) {
assert_eq!(raw1, raw2);
}
pub fn test_upload_revoked_tpk<D: Database>(db: &mut D) {
pub fn test_upload_revoked_tpk(db: &mut impl Database, log_path: &Path) {
let str_uid1 = "Test A <test_a@example.com>";
let str_uid2 = "Test B <test_b@example.com>";
let (mut tpk, revocation) = TPKBuilder::new()
@@ -513,6 +520,7 @@ pub fn test_upload_revoked_tpk<D: Database>(db: &mut D) {
.unwrap();
let email1 = Email::from_str(str_uid1).unwrap();
let email2 = Email::from_str(str_uid2).unwrap();
let fpr = Fingerprint::try_from(tpk.fingerprint()).unwrap();
tpk = tpk.merge_packets(vec![revocation.into()]).unwrap();
match tpk.revocation_status() {
@@ -522,6 +530,7 @@ pub fn test_upload_revoked_tpk<D: Database>(db: &mut D) {
// upload key
let tpk_status = db.merge(tpk).unwrap().into_tpk_status();
check_log_entry(log_path, &fpr);
assert_eq!(TpkStatus {
is_revoked: true,
email_status: vec!(
@@ -532,7 +541,7 @@ pub fn test_upload_revoked_tpk<D: Database>(db: &mut D) {
}, tpk_status);
}
pub fn test_uid_revocation<D: Database>(db: &mut D) {
pub fn test_uid_revocation(db: &mut impl Database, log_path: &Path) {
use std::{thread, time};
let str_uid1 = "Test A <test_a@example.com>";
@@ -550,6 +559,7 @@ pub fn test_uid_revocation<D: Database>(db: &mut D) {
// upload key
let tpk_status = db.merge(tpk.clone()).unwrap().into_tpk_status();
check_log_entry(log_path, &fpr);
assert_eq!(TpkStatus {
is_revoked: false,
email_status: vec!(
@@ -603,7 +613,7 @@ pub fn test_uid_revocation<D: Database>(db: &mut D) {
}
/* FIXME I couldn't get this to work.
pub fn test_uid_revocation_fake<D: Database>(db: &mut D) {
pub fn test_uid_revocation_fake(db: &mut D) {
use std::{thread, time};
let str_uid = "Test A <test_a@example.com>";
@@ -682,7 +692,7 @@ pub fn test_uid_revocation_fake<D: Database>(db: &mut D) {
}
*/
pub fn test_unlink_uid<D: Database>(db: &mut D) {
pub fn test_unlink_uid(db: &mut impl Database, log_path: &Path) {
let uid = "Test A <test_a@example.com>";
let email = Email::from_str(uid).unwrap();
@@ -696,6 +706,7 @@ pub fn test_unlink_uid<D: Database>(db: &mut D) {
// Create a 2nd key with same uid, and revoke the uid.
let tpk_evil = TPKBuilder::new().add_userid(uid).generate().unwrap().0;
let fpr_evil = Fingerprint::try_from(tpk_evil.fingerprint()).unwrap();
let sig = {
let uid = tpk_evil.userids()
.find(|b| b.userid().value() == uid.as_bytes()).unwrap();
@@ -715,6 +726,7 @@ pub fn test_unlink_uid<D: Database>(db: &mut D) {
assert_eq!(sig.sigtype(), SignatureType::CertificateRevocation);
let tpk_evil = tpk_evil.merge_packets(vec![sig.into()]).unwrap();
let tpk_status = db.merge(tpk_evil).unwrap().into_tpk_status();
check_log_entry(log_path, &fpr_evil);
assert_eq!(TpkStatus {
is_revoked: false,
email_status: vec!(
@@ -737,7 +749,7 @@ pub fn get_userids(armored: &str) -> Vec<UserID> {
// If multiple keys have the same email address, make sure things work
// as expected.
pub fn test_same_email_1<D: Database>(db: &mut D) {
pub fn test_same_email_1(db: &mut impl Database, log_path: &Path) {
let str_uid1 = "A <test@example.com>";
let tpk1 = TPKBuilder::new()
.add_userid(str_uid1)
@@ -760,6 +772,7 @@ pub fn test_same_email_1<D: Database>(db: &mut D) {
// upload keys.
let tpk_status1 = db.merge(tpk1).unwrap().into_tpk_status();
check_log_entry(log_path, &fpr1);
assert_eq!(TpkStatus {
is_revoked: false,
email_status: vec!(
@@ -768,6 +781,7 @@ pub fn test_same_email_1<D: Database>(db: &mut D) {
unparsed_uids: 0,
}, tpk_status1);
let tpk_status2 = db.merge(tpk2.clone()).unwrap().into_tpk_status();
check_log_entry(log_path, &fpr2);
assert_eq!(TpkStatus {
is_revoked: false,
email_status: vec!(
@@ -814,6 +828,7 @@ pub fn test_same_email_1<D: Database>(db: &mut D) {
assert_eq!(sig.sigtype(), SignatureType::CertificateRevocation);
let tpk2 = tpk2.merge_packets(vec![sig.into()]).unwrap();
let tpk_status2 = db.merge(tpk2).unwrap().into_tpk_status();
check_log_entry(log_path, &fpr2);
assert_eq!(TpkStatus {
is_revoked: false,
email_status: vec!(
@@ -829,7 +844,7 @@ pub fn test_same_email_1<D: Database>(db: &mut D) {
// If a key has multiple user ids with the same email address, make
// sure things still work.
pub fn test_same_email_2<D: Database>(db: &mut D) {
pub fn test_same_email_2(db: &mut impl Database, log_path: &Path) {
use std::{thread, time};
let str_uid1 = "A <test@example.com>";
@@ -847,6 +862,7 @@ pub fn test_same_email_2<D: Database>(db: &mut D) {
// upload key
let tpk_status = db.merge(tpk.clone()).unwrap().into_tpk_status();
check_log_entry(log_path, &fpr);
// verify uid1
assert_eq!(TpkStatus {
@@ -883,6 +899,7 @@ pub fn test_same_email_2<D: Database>(db: &mut D) {
assert_eq!(sig.sigtype(), SignatureType::CertificateRevocation);
let tpk = tpk.merge_packets(vec![sig.into()]).unwrap();
let tpk_status = db.merge(tpk).unwrap().into_tpk_status();
check_log_entry(log_path, &fpr);
assert_eq!(TpkStatus {
is_revoked: false,
email_status: vec!(
@@ -899,7 +916,7 @@ pub fn test_same_email_2<D: Database>(db: &mut D) {
vec![ uid1.clone() ]);
}
pub fn test_bad_uids<D: Database>(db: &mut D) {
pub fn test_bad_uids(db: &mut impl Database, log_path: &Path) {
let str_uid1 = "foo@bar.example <foo@bar.example>";
let str_uid2 = "A <test@example.com>";
let str_uid3 = "lalalalaaaaa";
@@ -911,33 +928,39 @@ pub fn test_bad_uids<D: Database>(db: &mut D) {
.unwrap()
.0;
let fpr = Fingerprint::try_from(tpk.fingerprint()).unwrap();
let email1 = Email::from_str(str_uid1).unwrap();
let email2 = Email::from_str(str_uid2).unwrap();
let tpk_status = db.merge(tpk).unwrap().into_tpk_status();
check_log_entry(log_path, &fpr);
assert_eq!(TpkStatus {
is_revoked: false,
email_status: vec!(
(email1.clone(), EmailAddressStatus::NotPublished),
(email2.clone(), EmailAddressStatus::NotPublished),
),
unparsed_uids: 2,
unparsed_uids: 1,
}, tpk_status);
db.set_email_published(&fpr, &email2).unwrap();
let tpk_status = db.get_tpk_status(&fpr, &vec!(email2.clone())).unwrap();
let tpk_status = db.get_tpk_status(&fpr, &vec!(email1.clone(),
email2.clone())).unwrap();
assert_eq!(TpkStatus {
is_revoked: false,
email_status: vec!(
(email1.clone(), EmailAddressStatus::NotPublished),
(email2.clone(), EmailAddressStatus::Published),
),
unparsed_uids: 2,
unparsed_uids: 1,
}, tpk_status);
}
pub fn test_no_selfsig<D: Database>(db: &mut D) {
pub fn test_no_selfsig(db: &mut impl Database, log_path: &Path) {
let (mut tpk, revocation) = TPKBuilder::new()
.generate()
.unwrap();
let fpr = Fingerprint::try_from(tpk.fingerprint()).unwrap();
// don't allow upload of naked key
assert!(db.merge(tpk.clone()).is_err());
@@ -945,9 +968,21 @@ pub fn test_no_selfsig<D: Database>(db: &mut D) {
// with revocation, it's ok
tpk = tpk.merge_packets(vec![revocation.into()]).unwrap();
let tpk_status = db.merge(tpk).unwrap().into_tpk_status();
check_log_entry(log_path, &fpr);
assert_eq!(TpkStatus {
is_revoked: true,
email_status: vec!(),
unparsed_uids: 0,
}, tpk_status);
}
fn check_log_entry(log_path: &Path, fpr: &Fingerprint) {
let log_data = fs::read_to_string(log_path).unwrap();
let last_entry = log_data
.lines()
.last().unwrap()
.split(" ")
.last().unwrap();
assert_eq!(last_entry, fpr.to_string());
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

18
dist/assets/img/atom.svg vendored Normal file
View File

@@ -0,0 +1,18 @@
<?xml version="1.0"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="128px" height="128px" id="RSSicon" viewBox="0 0 256 256">
<defs>
<linearGradient x1="0.085" y1="0.085" x2="0.915" y2="0.915" id="RSSg">
<stop offset="0.0" stop-color="#E3702D"/><stop offset="0.1071" stop-color="#EA7D31"/>
<stop offset="0.3503" stop-color="#F69537"/><stop offset="0.5" stop-color="#FB9E3A"/>
<stop offset="0.7016" stop-color="#EA7C31"/><stop offset="0.8866" stop-color="#DE642B"/>
<stop offset="1.0" stop-color="#D95B29"/>
</linearGradient>
</defs>
<rect width="256" height="256" rx="55" ry="55" x="0" y="0" fill="#CC5D15"/>
<rect width="246" height="246" rx="50" ry="50" x="5" y="5" fill="#F49C52"/>
<rect width="236" height="236" rx="47" ry="47" x="10" y="10" fill="url(#RSSg)"/>
<circle cx="68" cy="189" r="24" fill="#FFF"/>
<path d="M160 213h-34a82 82 0 0 0 -82 -82v-34a116 116 0 0 1 116 116z" fill="#FFF"/>
<path d="M184 213A140 140 0 0 0 44 73 V 38a175 175 0 0 1 175 175z" fill="#FFF"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
dist/assets/img/gpgtools.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

1
dist/assets/js/upload-verify.js vendored Normal file
View File

@@ -0,0 +1 @@
document.getElementById("postform").submit();

14
dist/assets/js/upload.js vendored Normal file
View File

@@ -0,0 +1,14 @@
var body = document.getElementsByTagName("body")[0];
body.addEventListener('dragover', function(e) {
e.preventDefault();
e.stopPropagation();
}, false);
body.addEventListener('drop', function(e) {
e.preventDefault();
e.stopPropagation();
if (e.dataTransfer) {
document.getElementById('keytext').files = e.dataTransfer.files;
}
}, false);

40
dist/assets/site.css vendored
View File

@@ -9,13 +9,13 @@ body {
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
src: url('fonts/roboto-v18-latin-300.eot');
src: url('fonts/roboto-v20-latin_latin-ext-300.eot');
src: local('Roboto Light'), local('Roboto-Light'),
url('fonts/roboto-v18-latin-300.eot?#iefix') format('embedded-opentype'),
url('fonts/roboto-v18-latin-300.woff2') format('woff2'),
url('fonts/roboto-v18-latin-300.woff') format('woff'),
url('fonts/roboto-v18-latin-300.ttf') format('truetype'),
url('fonts/roboto-v18-latin-300.svg#Roboto') format('svg');
url('fonts/roboto-v20-latin_latin-ext-300.eot?#iefix') format('embedded-opentype'),
url('fonts/roboto-v20-latin_latin-ext-300.woff2') format('woff2'),
url('fonts/roboto-v20-latin_latin-ext-300.woff') format('woff'),
url('fonts/roboto-v20-latin_latin-ext-300.ttf') format('truetype'),
url('fonts/roboto-v20-latin_latin-ext-300.svg#Roboto') format('svg');
}
tt {
@@ -43,11 +43,23 @@ a.brand {
color: #050505;
}
h3:target {
.usage h2 div {
display: inline-block;
width: 2.5em;
text-align: center;
}
.usage h2 div img {
display: inline-block;
height: 2em;
vertical-align: -30%;
}
h4:target, h3:target {
background-color: #ffa;
}
h3 a, h3 a:visited {
h4 a, h4 a:visited, h3 a, h3 a:visited {
color: #050505;
}
@@ -221,6 +233,7 @@ span.fingerprint {
span.email {
font-family: monospace;
font-size: large;
white-space: nowrap;
}
.debug_link {
@@ -254,3 +267,14 @@ span.email {
.attribution a {
color: #ccc;
}
input.textbutton {
padding: 0px;
border: none;
background: none;
margin: 0px;
cursor: pointer;
color: blue;
font-family: inherit;
font-size: inherit;
}

25
dist/email-templates/manage.htm.hbs vendored Normal file
View File

@@ -0,0 +1,25 @@
<!doctype html>
<html lang="{{lang}}">
<head>
<meta charset=utf-8>
<title>{{ text "Manage your key on {{domain}}" rerender }}</title>
</head>
<body>
<p>
{{ text "Hi," }}
<p>
{{ text "This is an automated message from <a href=\"{{base_uri}}\" style=\"text-decoration:none; color: #333\">{{domain}}</a>." rerender }}
{{ text "If you didn't request this message, please ignore it." }}
<p>
{{ text "OpenPGP key: <tt>{{primary_fp}}</tt>" rerender }}
<p>
{{ text "To manage and delete listed addresses on this key, please follow the link below:" }}
<p>
<a rel="nofollow" href="{{uri}}">{{uri}}</a>
<p>
{{ text "You can find more info at <a href=\"{{base_uri}}/about\">{{domain}}/about</a>." rerender }}
<p>
<a href="{{base_uri}}">{{base_uri}}</a><br />
{{ text "distributing OpenPGP keys since 2019" }}
</body>
</html>

17
dist/email-templates/manage.txt.hbs vendored Normal file
View File

@@ -0,0 +1,17 @@
{{ text "Hi," }}
{{ text "This is an automated message from {{domain}}." rerender }}
{{ text "If you didn't request this message, please ignore it." }}
{{ text "OpenPGP key: {{primary_fp}}" rerender }}
{{ text "To manage and delete listed addresses on this key, please follow the link below:" }}
{{ uri }}
{{ text "You can find more info at {{base_uri}}/about" rerender }}
--
{{ base_uri }}
{{ text "distributing OpenPGP keys since 2019" }}

25
dist/email-templates/verify.htm.hbs vendored Normal file
View File

@@ -0,0 +1,25 @@
<!doctype html>
<html lang="{{lang}}">
<head>
<meta charset=utf-8>
<title>{{ text "Verify {{userid}} for your key on {{domain}}" rerender }}</title>
</head>
<body>
<p>
{{ text "Hi," }}
<p>
{{ text "This is an automated message from <a href=\"{{base_uri}}\" style=\"text-decoration:none; color: #333\">{{domain}}</a>." rerender }}
{{ text "If you didn't request this message, please ignore it." }}
<p>
{{ text "OpenPGP key: <tt>{{primary_fp}}</tt>" rerender }}
<p>
{{ text "To let others find this key from your email address \"<a rel=\"nofollow\" href=\"#\" style=\"text-decoration:none; color: #333\">{{userid}}</a>\", please click the link below:" rerender }}
<p>
<a rel="nofollow" href="{{uri}}">{{uri}}</a>
<p>
{{ text "You can find more info at <a href=\"{{base_uri}}/about\">{{domain}}/about</a>." rerender }}
<p>
<a href="{{base_uri}}">{{base_uri}}</a><br />
{{ text "distributing OpenPGP keys since 2019" }}
</body>
</html>

17
dist/email-templates/verify.txt.hbs vendored Normal file
View File

@@ -0,0 +1,17 @@
{{ text "Hi," }}
{{ text "This is an automated message from {{domain}}." rerender }}
{{ text "If you didn't request this message, please ignore it." }}
{{ text "OpenPGP key: {{primary_fp}}" rerender }}
{{ text "To let others find this key from your email address \"{{userid}}\",\nplease follow the link below:" rerender }}
{{uri}}
{{ text "You can find more info at {{base_uri}}/about" rerender }}
--
{{ base_uri }}
{{ text "distributing OpenPGP keys since 2019" }}

26
dist/email-templates/welcome.htm.hbs vendored Normal file
View File

@@ -0,0 +1,26 @@
<!doctype html>
<html lang="{{lang}}">
<head>
<meta charset=utf-8>
<title>Your key upload on {{domain}}</title>
</head>
<body>
<p>
Hi,
<p>
This is an automated message from <a href="{{base_uri}}" style="text-decoration:none; color: #333">{{domain}}</a>.
If you didn't upload your key there, please ignore this message.
<p>
OpenPGP key: <tt>{{primary_fp}}</tt>
<p>
This key was just uploaded for the first time, and is now published without identity information. If you want to allow others to find this key by e-mail address, please follow this link:
<p>
<a rel="nofollow" href="{{uri}}">{{uri}}</a>
<p>
You can find more info at <a href="{{base_uri}}/about">{{domain}}/about</a>.
<p>
<a href="{{base_uri}}">{{base_uri}}</a><br />
distributing OpenPGP keys since 2019
</body>
</html>

19
dist/email-templates/welcome.txt.hbs vendored Normal file
View File

@@ -0,0 +1,19 @@
Hi,
This is an automated message from {{domain}}.
If you didn't upload your key, please ignore this message.
OpenPGP key: {{primary_fp}}
This key was just uploaded for the first time, and is now published without
identity information. If you want to allow others to find this key by e-mail
address, please follow this link:
{{uri}}
You can find more info at {{base_uri}}/about
--
{{ base_uri }}
distributing OpenPGP keys since 2019

View File

@@ -0,0 +1,2 @@
<p><strong>Error 400</strong> Invalid request</p>
<p>See https://keys.openpgp.org/about/api</p>

View File

@@ -0,0 +1,2 @@
<p><strong>Error 400</strong> Invalid request</p>
<p>See https://keys.openpgp.org/about/api</p>

1
dist/errors-static/404-by-email.htm vendored Normal file
View File

@@ -0,0 +1 @@
No key found for this email address.

1
dist/errors-static/404-by-fpr.htm vendored Normal file
View File

@@ -0,0 +1 @@
No key found for this fingerprint.

1
dist/errors-static/404-by-keyid.htm vendored Normal file
View File

@@ -0,0 +1 @@
No key found for this key id.

View File

@@ -0,0 +1,2 @@
<p><strong>Error 429</strong> Too many requests</p>
<p>See https://keys.openpgp.org/about/api#rate-limiting</p>

View File

@@ -0,0 +1,2 @@
<p><strong>Error 429</strong> Too many requests</p>
<p>See https://keys.openpgp.org/about/api#rate-limiting</p>

View File

@@ -1 +1 @@
Bad request: {{error}}
Bad request: {{ page/error }}

5
dist/templates/400.html.hbs vendored Normal file
View File

@@ -0,0 +1,5 @@
{{#> layout }}
<h3>{{ text "There was an error with your request:" }}</h3>
<p>{{ page/error }}</p>
{{/layout}}

View File

@@ -1,7 +1,7 @@
{{#> layout }}
<h2>Error</h2>
<h2>{{ text "Error" }}</h2>
<p>Looks like something went wrong :(</p>
<p>{{ text "Looks like something went wrong :(" }}</p>
<p><strong>Error:</strong> {{internal_error}}</p>
<p>{{ text "Error message: {{ internal_error }}" rerender }}</p>
{{/layout}}

View File

@@ -21,7 +21,7 @@
<ul>
<li><strong>Identity information</strong> describes the parts of
a key that identify its owner, also known as "User IDs".
A User ID typically includes a name and an e-mail address.
A User ID typically includes a name and an email address.
</li>
<li><strong>Non-identity information</strong> is all the technical
information about the key itself. This includes the large numbers
@@ -54,9 +54,9 @@
The <strong>identity information</strong> in an OpenPGP key
is only distributed with consent.
It contains personal data, and is not strictly necessary for
a key to be used for encryption or signature verification. Once the
owner gives consent by verifying their e-mail address, the key can
be found by everyone via search by address.
a key to be used for encryption or signature verification.
Once the owner gives consent by verifying their email address,
the key can be found via search by address.
</p>
<h3 id="community">Community and platform</h3>
@@ -66,7 +66,7 @@
You can talk to us in
#hagrid on Freenode IRC,
also reachable as #hagrid:stratum0.org on Matrix.
Of course you can also reach us via e-mail,
Of course you can also reach us via email,
at <tt>support at keys dot openpgp dot org</tt>.
The folks who are running this come
from various projects in the OpenPGP ecosystem,
@@ -74,13 +74,12 @@
</p>
<p>
Technically, <tt>keys.openpgp.org</tt> runs on the
<a href="https://gitlab.com/hagrid-keyserver/hagrid" target="_blank">Hagrid</a>
keyserver software, which is based on
<a href="https://sequoia-pgp.org">Sequoia-PGP</a>. We are hosted on the
<a href="https://eclips.is" target="_blank">eclips.is</a> platform,
a hosting provider focused on Internet Freedom projects, run by
<a href="https://greenhost.net/" target="_blank">Greenhost</a>.
Technically,
<tt>keys.openpgp.org</tt> runs on the <a href="https://gitlab.com/hagrid-keyserver/hagrid" target="_blank">Hagrid</a> keyserver software,
which is based on <a href="https://sequoia-pgp.org">Sequoia-PGP</a>.
We are running on <a href="https://eclips.is" target="_blank">eclips.is</a>,
a hosting platform focused on Internet Freedom projects,
which is managed by <a href="https://greenhost.net/" target="_blank">Greenhost</a>.
</p>
</div>
{{/layout}}

View File

@@ -37,9 +37,9 @@
<li>
<tt>GET /vks/v1/by-email/&lt;URI-ENCODED EMAIL-ADDRESS&gt;</tt>
<p>
Retrieves the key with the given <tt>E-Mail Address</tt>.
Retrieves the key with the given <tt>Email Address</tt>.
Only exact matches are accepted.
Lookup by e-mail address requires opt-in by the owner of the e-mail address.
Lookup by email address requires opt-in by the owner of the email address.
The returned key is ASCII Armored, and has a content-type of <code>application/pgp-keys</code>.
</p>
</li>
@@ -68,7 +68,7 @@
<code>published</code>,
<code>revoked</code>, or
<code>pending</code>,
indicating the status of this e-mail address.
indicating the status of this email address.
</p>
<div class="example">
@@ -99,23 +99,23 @@
<tt>POST /vks/v1/request-verify</tt>
<p>
A key that has been uploaded
can be made discoverable by one or more of its e-mail addresses
can be made discoverable by one or more of its email addresses
by proving ownership of the address
via a verification e-mail.
via a verification email.
This endpoint requests verification
for one or more e-mail addresses.
for one or more email addresses.
</p>
<p>
The body of the request must be <code>application/json</code>.
The JSON data must include the opaque <code>token</code> value
(obtained via <tt>/vks/v1/upload</tt>)
and an <code>addresses</code> field,
which is a list of e-mail addresses (not full User IDs)
which is a list of email addresses (not full User IDs)
to request verification mails for.
It can optionally include a <code>locale</code> field,
which is list of locales,
ordered by preference,
which to use for the verification e-mail.
which to use for the verification email.
The reply will be the same as for the <tt>/vks/v1/upload</tt> endpoint,
with addresses marked as <code>pending</code> where a verification email
has been sent.
@@ -183,6 +183,23 @@
</div>
</div>
<h3 id="rate-limiting"><a href="#rate-limiting">Rate Limiting</a></h3>
<p>
Requests to the <span class="brand">keys.openpgp.org</span> API are rate
limited:
</p>
<ul>
<li>Requests by fingerprint or key id are limited to a rate of five requests
per second. Excessive requests are delayed to match this rate. Short
bursts are allowed.
</li>
<li>Requests by email address are limited to one request per minute.
Excessive requests will fail with <tt>error 429</tt>. Short bursts are
allowed.
</li>
</ul>
<h2>HTTP Keyserver Protocol (HKP) Interface</h2>
<p>
Hagrid implements a subset of
@@ -238,13 +255,16 @@
<code>keytext</code> must be the keys to submit,
which must be ASCII Armored.
More than one key may be submitted in one request.
For verification of e-mail addresses,
For verification of email addresses,
the <tt>/vks/v1/upload</tt> endpoint
must be used instead.
</p>
</li>
</ul>
<a href="#rate-limiting">Rate limiting</a> applies as for the VKS interface
above.
<h4>Limitations</h4>
<p>
@@ -253,14 +273,14 @@
</p>
<ul>
<li>No support for <code>op=vindex</code>,</li>
<li>only exact matches for user IDs are returned (i.e. <code>exact=on</code> is
always assumed),</li>
<li>the <code>fingerprint</code> variable is ignored,</li>
<li>the <code>nm</code> option is ignored,</li>
<li><code>op=index</code> returns either one or no keys,</li>
<li>uploads are restricted to 1 MiB,</li>
<li>all packets that aren't public keys, user IDs or signatures are filtered out.</li>
<li>No support for <code>op=vindex</code>.</li>
<li>Only exact matches by email address, fingerprint or long key id are returned.</li>
<li>All requests return either one or no keys.</li>
<li>The expiration date field in <code>op=index</code> is left blank (discussion <a target="_blank" href="https://gitlab.com/hagrid-keyserver/hagrid/issues/134">here</a>).</li>
<li>All parameters and options other than <code>op</code> and <code>search</code> are ignored.</li>
<li>Output is always machine readable (i.e. <code>options=mr</code> is always assumed).</li>
<li>Uploads are restricted to 1 MiB.</li>
<li>All packets that aren't public keys, user IDs or signatures are filtered out.</li>
</ul>
</div>

View File

@@ -35,7 +35,7 @@
<li>
Federation with open participation requires all data to be public.
This significantly impacts the privacy of our users, because it
allows anyone to scrape a list of all e-mail addresses.
allows anyone to scrape a list of all email addresses.
</li>
<li>
Servers run as a hobby by casual administrators do not meet our
@@ -44,25 +44,25 @@
</ol>
<h3 id="non-email-uids"><a href="#non-email-uids">Why is there no support
for identities that aren't e-mail addresses?</a></h3>
for identities that aren't email addresses?</a></h3>
<p>
We require explicit consent to distribute identity information.
Identities that aren't e-mail addresses, such as pictures or website
Identities that aren't email addresses, such as pictures or website
URLs, offer no simple way for us to acquire this consent.
</p>
<p>
Note: Some OpenPGP software creates keys with incorrectly formatted
e-mail addresses. These addresses might not be recognized correctly on
email addresses. These addresses might not be recognized correctly on
<span class="brand">keys.openpgp.org</span>.
</p>
<h3 id="verify-multiple"><a href="#verify-multiple">Can I verify more than
one key for some e-mail address?</a></h3>
one key for some email address?</a></h3>
<p>
An e-mail address can only be associated with a single key.
An email address can only be associated with a single key.
When an address is verified for a new key,
it will no longer appear in any key
for which it was previously verified.
@@ -71,16 +71,16 @@
</p>
<p>
This means a search by e-mail address
This means a search by email address
will only return a single key,
not multiple candidates.
This eliminates an impossible choice for the user
("Which key is the right one?"),
and makes key discovery by e-mail much more convenient.
and makes key discovery by email much more convenient.
</p>
<h3 id="email-protection"><a href="#email-protection">What do you do to
protect outgoing verification e-mails?</a></h3>
protect outgoing verification emails?</a></h3>
<p>
We use a modern standard called
@@ -88,14 +88,14 @@
combined with
<a href="https://starttls-everywhere.org/" target="_blank">STARTTLS Everywhere</a>
by the EFF,
to make sure verification e-mails are sent out securely.
to make sure verification emails are sent out securely.
This protects against eavesdropping and interception during delivery.
</p>
<p>
The MTA-STS mechanism depends on correctly configured e-mail servers.
The MTA-STS mechanism depends on correctly configured email servers.
You can <a href="https://www.hardenize.com/">run this test</a>
to see if your e-mail provider supports it.
to see if your email provider supports it.
If the "MTA-STS" entry on the left isn't a green checkmark,
please ask your provider to update their configuration.
</p>
@@ -148,9 +148,9 @@
<p>
The <span class="brand">keys.openpgp.org</span> service is meant for key
distribution and discovery, not as a de-facto CA. Client implementations
that want to offer verified communication should rely on their own trust
model.
distribution and discovery, not as a de facto certification authority.
Client implementations that want to offer verified communication should
rely on their own trust model.
</p>
<h3 id="revoked-uids"><a href="#revoked-uids">Why are revoked identities not
@@ -158,7 +158,7 @@
<p>
When an OpenPGP key marks one of its identities as revoked, this
identity should no longer be considered valid for the key. And this
identity should no longer be considered valid for the key, and this
information should ideally be distributed to all OpenPGP clients that
already know about the newly revoked identity.
</p>
@@ -177,6 +177,25 @@
we can.
</p>
<h3 id="search-substring"><a href="#search-substring">Why isn't it possible to search by part of an email address, like just the domain?</a></h3>
<p>
Some keyservers support search for keys by part of an email address.
This allows discovery not only of keys, but also of addresses, with a query like "keys for addresses at gmail dot com".
This effectively puts the addresses of all keys on those keyservers into a public listing.
</p>
<p>
Searches by email address on <span class="brand">keys.openpgp.org</span> return only exact matches.
That way, users can discover keys for addresses they already know, but not new addresses.
This ensures malicious users and spammers can't obtain a list of all addresses on the server.
</p>
<p>
We made this restriction a part of our <a href="/about/privacy">privacy policy</a>,
which means we can't change it without asking for user consent.
</p>
<h3 id="tor"><a href="#tor">Do you support Tor?</a></h3>
<p>
@@ -184,13 +203,13 @@
If you have Tor installed,
you can reach <span class="brand">keys.openpgp.org</span> anonymously
as an
<a href="https://en.wikipedia.org/wiki/Tor_(anonymity_network)#Onion_services" target="_blank">onion service</a>:
<a href="https://support.torproject.org/onionservices/#onionservices-2" target="_blank">onion service</a>:
<br />
<a href="http://zkaan2xfbuxia2wpf7ofnkbz6r5zdbbvxbunvp5g2iebopbfc4iqmbad.onion">zkaan2xfbuxia2wpf7ofnkbz6r5zdbbvxbunvp5g2iebopbfc4iqmbad.onion</a>
</p>
<h3 id="encrypt-verification-emails"><a href="#encrypt-verification-emails">
Why not encrypt verification e-mails?</a></h3>
Why not encrypt verification emails?</a></h3>
Various reasons:
<ol>
@@ -214,8 +233,8 @@
to process the key:
</p>
<blockquote>
$ gpg --receive-keys A2604867523C7ED8<br />
gpg: key A2604867523C7ED8: no user ID
$ gpg --receive-keys EB85BB5FA33A75E15E944E63F231550C4F47E38E<br />
gpg: key EB85BB5FA33A75E15E944E63F231550C4F47E38E: no user ID
</blockquote>
<p>
We are working with the GnuPG team to resolve this problem.

View File

@@ -2,10 +2,238 @@
<div class="about">
<center><h2><a href="/about">About</a> | News | <a href="/about/usage">Usage</a> | <a href="/about/faq">FAQ</a> | <a href="/about/stats">Stats</a> | <a href="/about/privacy">Privacy</a></h2></center>
<h3>
<h2 id="2019-11-12-celebrating-100k">
<div style="float: right; font-size: small; line-height: 2em;">2019-11-12 📅</div>
<a style="color: black;" href="/about/news#2019-11-12-celebrating-100k">Celebrating 100.000 verified addresses! 📈</a>
</h2>
<p>
Five months ago, we launched this service.
And just today, we have reached a remarkable milestone:
<center style="margin-top: 2em; margin-bottom: 2em;">
<img src="/assets/img/stats-addresses-2019-11-12.png" style="padding: 1px; border: 1px solid gray;" /><br />
<strong>One hundred thousand verified email addresses!</strong>
</center>
<p>
Thanks to everyone using this service!
And thanks especially to those who have provided us with feedback,
translations, or even code contributions!
<p>
A few updates on things we've been working on:
<ul>
<li>
This news page is now available as an <strong><a target="_blank" href="/atom.xml">atom feed <img src="/assets/img/atom.svg" style="height: 0.8em;" /></a></strong>.
</li>
<li>
We have been working on
a <strong><a target="_blank" href="https://gitlab.com/hagrid-keyserver/hagrid/issues/131">new mechanism to refresh keys</a></strong>
that better protects the user's privacy.
</li>
<li>
Work on <strong>localization</strong> is in full swing!
we hope to have some languages ready for deployment soon.
</li>
</ul>
<p>
If you would like to see <span class="brand">keys.openpgp.org</span>
translated into your native language,
please <a target="_blank" href="https://www.transifex.com/otf/hagrid/">join the translation team</a>
over on Transifex.
We would appreciate help especially for <strong>Russian</strong>, <strong>Italian</strong>, <strong>Polish</strong> and <strong>Dutch</strong>.
<p>
That's all, keeping this one short!
<span style="font-size: x-large;">👍️</span>
<hr style="margin-top: 2em; margin-bottom: 2em;" />
<h2 id="2019-09-12-three-months-later">
<div style="float: right; font-size: small; line-height: 2em;">2019-09-12 📅</div>
<a style="color: black;" href="/about/news#2019-09-12-three-months-later">Three months after launch ✨</a>
</h2>
<p>
It has been three months now
<a href="/about/news#2019-06-12-launch">since we launched</a>
<span class="brand">keys.openpgp.org</span>.
We are happy to report:
It has been a resounding success!
🥳
<h4>Adoption in clients</h4>
<p>
The
<span class="brand">keys.openpgp.org</span>
keyserver has been received very well by users,
and clients are adopting it rapidly.
It is now used by default in
<a href="https://gpgtools.org/" target="_blank">GPGTools</a>,
<a href="https://enigmail.net/" target="_blank">Enigmail</a>,
<a href="https://www.openkeychain.org/" target="_blank">OpenKeychain</a>,
<a href="https://github.com/firstlookmedia/gpgsync" target="_blank">GPGSync</a>,
Debian,
NixOS,
and others.
Many tutorials have also been updated,
pointing users our way.
<p>
At the time of writing,
more than 70.000 email addresses
have been verified.
<center style="margin-top: 2em; margin-bottom: 2em;">
<img src="/assets/img/stats-addresses-2019-09-12.png" style="padding: 1px; border: 1px solid gray;" /><br />
<span style="font-size: smaller;">If that isn't a promising curve, I don't know what is :)</span>
</center>
<p>
A special shout-out here goes to GPGTools for macOS.
They implemented the update process so smoothly,
the number of verified addresses completely exploded
when they released their update.
<h4>All's good in operations</h4>
<p>
There is not a lot to report operationally,
and no news is good news in this case!
Since launch,
there was nearly zero downtime,
only a single bug came up
that briefly caused issues during upload,
and support volume has been comfortably low.
<p>
Our traffic is currently
at about ten requests per second
(more during the day, less on the weekend),
and we delivered roughly 100.000 emails
in the last month.
No sweat.
<p>
We made several small operational improvements
including deployment of
<a href="http://dnsviz.net/d/keys.openpgp.org/dnssec/" target="_blank">DNSSEC</a>,
implementing some
<a href="/about/api#rate-limiting" target="_blank">rate-limiting</a>,
nailing down our
<a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP">content security policy</a>
headers,
and enabling
<a href="https://blog.torproject.org/whats-new-tor-0298" target="_blank">single-hop</a>
mode on our Tor Onion Service.
You can find a more complete list
<a href="https://gitlab.com/hagrid-keyserver/hagrid/merge_requests?scope=all&utf8=%E2%9C%93&state=merged" target="_blank">here</a>.
<h4>Secure email delivery with MTA-STS</h4>
<p>
One improvement that deserves special mention is
<a href="https://www.hardenize.com/blog/mta-sts">MTA-STS</a>,
which improves the security of outgoing emails.
<p>
While HTTPS is deployed fairly universally these days,
that sadly isn't the case for email.
Many servers don't do encryption at all,
or use a self-signed certificate
instead of a proper one (e.g. from Let's Encrypt).
But delivery failures upset customers more
than reduced security,
and many emails are still delivered without encryption.
<p>
With MTA-STS, domain operators can indicate
(via HTTPS)
that their email server <em>does</em> support encryption.
When a secure connection can't be established
to such a server,
message delivery will be postponed
or eventually bounce,
instead of proceeding insecurely.
<p>
This is extremely useful for service like
<span class="brand">keys.openpgp.org</span>.
If encryption isn't reliable,
attackers can intercept verification emails relatively easily.
But for providers who have MTA-STS deployed,
we can be sure that
every message is delivered securely,
and to the right server.
<p>
You can <a href="https://aykevl.nl/apps/mta-sts/" target="_blank">run a check</a>
to find out whether your email provider
supports MTA-STS.
If they don't,
please drop them a message and tell them
to step up their security game!
<h4>Work in progress</h4>
<p>
We are working on two features:
<p>
The first is <strong>localization</strong>.
Most people do not speak English,
but so far that is the only language we support.
To make this service more accessible,
we are working with the OTF's
<a href="https://www.opentech.fund/labs/localization-lab/" target="_blank">Localization Lab</a>
to make the website and outgoing emails
available in several more languages.
<p>
The second is to bring back
<strong>third-party signatures</strong>.
As <a href="/about/faq#third-party-signatures">mentioned in our FAQ</a>,
we currently don't support these due to spam and potential for abuse.
The idea is to require
<a href="https://gitlab.com/openpgp-wg/rfc4880bis/merge_requests/20/diffs" target="_blank">cross-signatures</a>,
which allow each key to choose for itself
which signatures from other people it wants to distribute.
Despite this extra step,
this is fairly compatible with existing software.
It also nicely stays out of the way of users
who don't care about signatures.
<p>
Although work is in progress for both of those features,
neither have a planned time of release yet.
<p>
Regarding the "<tt>no user ID</tt>" issue with GnuPG
(mentioned in our
<a href="/about/news#2019-06-12-launch-challenges">last news post</a>
and our
<a href="/about/faq#older-gnupg" target="_blank">FAQ</a>),
a patch that fixes this problem is now carried by Debian,
as well as GPGTools for macOS.
GnuPG upstream has not merged the patch so far.
<p>
That's it!
Thanks for your interest!
<span style="font-size: x-large;">👋</span>
<hr style="margin-top: 2em; margin-bottom: 2em;" />
<h2 id="2019-06-12-launch">
<div style="float: right; font-size: small; line-height: 2em;">2019-06-12 📅</div>
<a href="/about/news#2019-06-12-launch">Launching a new keyserver! 🚀</a>
</h3>
<a href="/about/news#2019-06-12-launch" style="color: black;">Launching a new keyserver! 🚀</a>
</h2>
<p>
From a community effort by
@@ -24,7 +252,7 @@
<li>Precise. Searches return only a single key, which allows for easy key discovery.</li>
<li>Validating. Identities are only published with consent,
while non-identity information is freely distributed.</li>
<li>Deletable. Users can delete personal information with a simple e-mail confirmation.</li>
<li>Deletable. Users can delete personal information with a simple email confirmation.</li>
<li>Built on Rust, powered by <a href="https://sequoia-pgp.org" target="_blank">Sequoia PGP</a> - free and open source, running AGPLv3.</li>
</ul>
@@ -64,16 +292,16 @@
that can also be revoked at any time.
<p>
If a new key is verified for some e-mail address,
If a new key is verified for some email address,
it will replace the previous one.
This way,
every e-mail address is only associated with a single key at most.
every email address is only associated with a single key at most.
It can also be removed from the listing
at any time by the owner of the address.
This is very useful for key discovery:
if a search by e-mail address returns a key,
if a search by email address returns a key,
it means this is the single key
that is currently valid for the searched e-mail address.
that is currently valid for the searched email address.
<h4>Support in Enigmail and OpenKeychain</h4>
@@ -85,11 +313,11 @@
<a href="https://play.google.com/store/apps/details?id=org.sufficientlysecure.keychain&hl=en">OpenKeychain</a> on Android.
This means users of those implementations will
benefit from the faster response times,
and improved key discovery by e-mail address.
and improved key discovery by email address.
We hope that this will also give us some momentum
to build this project into a bigger community effort.
<h4>Current challenges</h4>
<h4 id="2019-06-12-launch-challenges">Current challenges</h4>
<p>
Privacy-preserving techniques in keyservers are still new,
@@ -127,6 +355,7 @@
and an <a target="_blank" href="/about/faq#tor">Onion Service</a>!
<p>
Cheers! 🍻
Cheers!
<span style="font-size: x-large;">🍻</span>
</div>
{{/layout}}

View File

@@ -7,8 +7,8 @@
distributes OpenPGP key data. The specific way in which data is processed
differs by type as follows:
<ul>
<li><b>E-Mail Addresses</b>
<p>E-Mail addresses contained in <abbr title="Packet Tag 13">User
<li><b>Email Addresses</b>
<p>Email addresses contained in <abbr title="Packet Tag 13">User
IDs</abbr> are personally identifiable information (PII).
Special care is taken to make sure they are used only with
consent:
@@ -17,9 +17,9 @@
Publishing requires <a target="_blank"
href="https://en.wikipedia.org/wiki/Opt-in_email#Confirmed_opt-in_(COI)_/_Double_opt-in_(DOI)">double
opt-in</a> validation, to prove ownership of the
e-mail address in question.
email address in question.
</li>
<li>Addresses are searchable by exact E-Mail address,
<li>Addresses are searchable by exact email address,
but not by associated name.</li>
<li>Enumeration of addresses is not possible.</li>
<li>Deletion of addresses is possible via simple proof
@@ -47,9 +47,9 @@
</p>
</li>
<li><b>Other User ID data</b>
<p>An OpenPGP key may contain personal data other than E-Mail
<p>An OpenPGP key may contain personal data other than email
addresses, such as <abbr title="Packet Tag 13">User IDs</abbr>
that do not contain E-Mail addresses, or <abbr
that do not contain email addresses, or <abbr
title="Packet Tag 17">image attributes</abbr>. This data is stripped
during upload and never stored, processed, or distributed in
any way.

View File

@@ -2,18 +2,18 @@
<div class="about">
<center><h2><a href="/about">About</a> | <a href="/about/news">News</a> | <a href="/about/usage">Usage</a> | <a href="/about/faq">FAQ</a> | Stats | <a href="/about/privacy">Privacy</a></h2></center>
<h3>Verified e-mail addresses</h3>
<h3>Verified email addresses</h3>
<p>
A simple statistic of the total number of e-mail addresses that are currently verified. 📈
A simple statistic of the total number of email addresses that are currently verified. 📈
</p>
<p>
<center><img src="/about/stats/week.png" /></center>
</p>
<p>
<center><img src="/about/stats/month.png" /></center>
</p>
<p>
<center><img src="/about/stats/year.png" /></center>
</p>
<h3>Load Average</h3>
@@ -31,4 +31,3 @@
</p>
</div>
{{/layout}}

View File

@@ -10,35 +10,67 @@
write to us and we'll try to add it.
</p>
<h2 style="margin-left: 3%;"><img src="/assets/img/enigmail.svg" style="display: inline; height: 2em; vertical-align: -30%; padding-right: 10px;" /> Enigmail</h2>
<h2>
<div><img src="/assets/img/enigmail.svg"></div>
Enigmail
</h2>
<p>
Enigmail will support <span class="brand">keys.openpgp.org</span> out
of the box in an upcoming version. Stay tuned!
<a href="https://enigmail.net" target="_blank">Enigmail</a> for Thunderbird
uses <span class="brand">keys.openpgp.org</span> by default since
version 2.0.12.
</p>
<p>Full support is available since Enigmail 2.1
(for <a href="https://www.thunderbird.net/en-US/thunderbird/68.0beta/releasenotes/" target="_blank">Thunderbird 68</a> or newer):
<ul>
<li>Keys will be kept up to date automatically.</li>
<li>During creation, you can optionally upload and verify your key.</li>
<li>During key creation, you can upload and verify your key.</li>
<li>Keys can be discovered by email address.</li>
</ul>
</p>
<h2 style="margin-left: 3%;"><img src="/assets/img/openkeychain.svg" style="display: inline; height: 2em; vertical-align: -30%; padding-right: 10px;" /> OpenKeychain</h2>
<h2>
<div><img src="/assets/img/gpgtools.png"></div>
GPG Suite
</h2>
<p>
OpenKeychain will support <span class="brand">keys.openpgp.org</span>
out of the box in an upcoming version. Stay tuned!
<a href="https://gpgtools.org/">GPG Suite</a> for macOS
uses <span class="brand">keys.openpgp.org</span> by default
since August 2019.
</p>
<h2>
<div><img src="/assets/img/openkeychain.svg"></div>
OpenKeychain
</h2>
<p>
<a href="https://www.openkeychain.org/">OpenKeychain</a> for Android
uses <span class="brand">keys.openpgp.org</span> by default
since July 2019.
<ul>
<li>Keys will be kept up to date automatically.</li>
<li>During creation, you can optionally upload and verify your key.</li>
<li>Keys can be discovered by email address.</li>
</ul>
</p>
<h2 style="margin-left: 3%;"><img src="/assets/img/gnupg.svg" style="display: inline; height: 2em; vertical-align: -30%; padding-right: 10px;" /> GnuPG</h2>
<p>
Note that there is no built-in support for upload and email address verification so far.
</p>
To configure GnuPG to use <span class="brand">keys.openpgp.org</span>
as keyserver, add this line to your <tt>gpg.conf</tt> file:
<h2>
<div><img src="/assets/img/gnupg.svg" /></div>
GnuPG
</h2>
<p>
To configure <a href="https://gnupg.org">GnuPG</a>
to use <span class="brand">keys.openpgp.org</span> as keyserver,
add this line to your <tt>gpg.conf</tt> file:
<blockquote>
keyserver hkps://keys.openpgp.org
</blockquote>
</p>
<h3>Retrieving keys</h3>
<h4 id="gnupg-retrieve"><a href="#gnupg-retrieve">Retrieving keys</a></h4>
<ul>
<li>
To locate the key of a user, by email address:
@@ -46,18 +78,14 @@
</li>
<li>To refresh all your keys (e.g. new revocation certificates and subkeys):
<blockquote>gpg --refresh-keys</blockquote>
<b>Note:</b> If you see errors like the following,
see <a href="/about/faq#older-gnupg">our notes</a> on compatibility
with older versions of GnuPG.
<blockquote>gpg: key A2604867523C7ED8: no user ID</blockquote>
</li>
</ul>
<h3 id="gnupg-upload">Uploading your key</h3>
<h4 id="gnupg-upload"><a href="#gnupg-upload">Uploading your key</a></h4>
<p>
Keys can be uploaded with GnuPG's <tt>--send-keys</tt> command, but
identity information can't be verified that way to make the key
searchable by e-mail address (<a href="/about">what's that?</a>).
searchable by email address (<a href="/about">what does this mean?</a>).
</p>
<ul>
@@ -69,19 +97,41 @@
</blockquote>
</li>
<li>
Alternatively, you can export them to a file and enter that into the
<a href="/upload" target="_blank">upload</a> page:
Alternatively, you can export them to a file
and select that file in the <a href="/upload" target="_blank">upload</a> page:
<blockquote>
gpg --export your_address@example.net &gt; my_key.pub
</blockquote>
</li>
</ul>
<h3>Usage via Tor</h3>
<h4 id="gnupg-troubleshooting"><a href="#gnupg-troubleshooting">Troubleshooting</a></h4>
<ul>
<li>
Some old <tt>~/gnupg/dirmngr.conf</tt> files contain a line like this:
<blockquote>
hkp-cacert ~/.gnupg/sks-keyservers.netCA.pem
</blockquote>
<p>
For users who want to be extra-careful,
This configuration is no longer necessary,
but prevents regular certificates from working.
It is recommended to simply remove this line from the configuration.
</p>
</li>
<li>
While refreshing keys, you may see errors like the following:
<blockquote>gpg: key A2604867523C7ED8: no user ID</blockquote>
This is a <a href="https://dev.gnupg.org/T4393" target="_blank">known problem in GnuPG</a>.
We are working with the GnuPG team to resolve this issue.
</li>
</ul>
<h4 id="gnupg-tor"><a href="#gnupg-tor">Usage via Tor</a></h4>
<p>
For users who want to be extra careful,
<span class="brand">keys.openpgp.org</span> can be reached anonymously as an
<a href="https://en.wikipedia.org/wiki/Tor_(anonymity_network)#Onion_services" target="_blank">onion service</a>.
<a href="https://support.torproject.org/onionservices/#onionservices-2" target="_blank">onion service</a>.
If you have
<a href="https://www.torproject.org/" target="_blank">Tor</a>
installed, use the following configuration:
@@ -101,7 +151,7 @@
<p>
Missing a guide for your favorite implementation? This site is
a work-in-progress, and we are looking to improve. Drop us a line at
a work-in-progress, and we are looking to improve it. Drop us a line at
<span class="email">support at keys dot openpgp dot org</span> if you
want to help out!
</p>

25
dist/templates/atom.xml.hbs vendored Normal file
View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>keys.openpgp.org</title>
<link href="{{ base_uri }}/atom.xml" rel="self"/>
<id>urn:uuid:8e783366-73b1-460e-83d3-42f01046646d</id>
<updated>2019-11-12T12:00:00Z</updated>
<entry>
<title>Celebrating 100.000 verified addresses! 📈</title>
<link href="{{ base_uri }}/about/news#2019-11-12-celebrating-100k" />
<updated>2019-11-12T12:00:00Z</updated>
<id>urn:uuid:5b69781f-5aa4-4276-8d9e-6a71c896cb65</id>
</entry>
<entry>
<title>Launching a new keyserver! 🚀</title>
<link href="{{ base_uri }}/about/news#2019-06-12-launch" />
<updated>2019-06-12T12:00:00Z</updated>
<id>urn:uuid:a071a6dc-f8ea-43de-b853-bd6d8bbe063f</id>
</entry>
<entry>
<title>Three months after launch ✨</title>
<link href="{{ base_uri }}/about/news#2019-09-12-three-months-later" />
<updated>2019-09-12T12:00:00Z</updated>
<id>urn:uuid:1bd5412a-f480-4c3f-a72d-c9b7a849f544</id>
</entry>
</feed>

View File

@@ -1,26 +0,0 @@
<!doctype html>
<html lang=en>
<head>
<meta charset=utf-8>
<title>Manage your key on {{domain}}</title>
</head>
<body>
<p>
Hi,
<p>
this is an automated message from <a rel="nofollow" href="{{base_uri}}" style="text-decoration:none; color: #333"><tt>{{domain}}</tt></a>. If you didn't
request this message, please ignore it.
<p>
OpenPGP key: <tt>{{primary_fp}}</tt>
<p>
To manage and delete listed addresses on this key, please follow
the link below:
<p>
<a href="{{uri}}">{{uri}}</a>
<p>
You can find more info at <a href="{{base_uri}}/about">{{domain}}/about</a>.
<p>
Greetings from the <a rel="nofollow" href="{{base_uri}}" style="text-decoration:none; color: #333"><tt>keys.openpgp.org</tt></a> team
</body>
</html>

View File

@@ -1,16 +0,0 @@
Hi,
this is an automated message from {{domain}}. If you didn't
request this message, please ignore it.
OpenPGP key: {{primary_fp}}
To manage and delete listed addresses on this key, please follow
the link below:
{{uri}}
You can find more info at {{base_uri}}/about
Greetings from the keys.openpgp.org team

View File

@@ -1,26 +0,0 @@
<!doctype html>
<html lang=en>
<head>
<meta charset=utf-8>
<title>Verify {{userid}} for your key on {{domain}}</title>
</head>
<body>
<p>
Hi,
<p>
this is an automated message from <a rel="nofollow" href="{{base_uri}}" style="text-decoration:none; color: #333"><tt>{{domain}}</tt></a>. If you didn't
request this message, please ignore it.
<p>
OpenPGP key: <tt>{{primary_fp}}</tt>
<p>
To let others find this key from your email address
"<a rel="nofollow" href="#" style="text-decoration:none; color: #333">{{userid}}</a>",
please click the link below:
<p>
<a href="{{uri}}">{{uri}}</a>
<p>
You can find more info at <a href="{{base_uri}}/about">{{domain}}/about</a>.
<p>
Greetings from the <a rel="nofollow" href="{{base_uri}}" style="text-decoration:none; color: #333"><tt>keys.openpgp.org</tt></a> team
</body>
</html>

View File

@@ -1,15 +0,0 @@
Hi,
this is an automated message from {{domain}}. If you didn't
request this message, please ignore it.
OpenPGP key: {{primary_fp}}
To let others find this key from your email address "{{userid}}",
please follow the link below:
{{uri}}
You can find more info at {{base_uri}}/about
Greetings from the keys.openpgp.org team

View File

@@ -0,0 +1,5 @@
{{#> layout }}
<h2>Too many requests!</h2>
<tt>Slow down a little</tt>
{{/layout}}

View File

@@ -1,20 +1,20 @@
{{#> layout }}
{{#with page}}
<div class="ui">
<p>
We found an entry for <span class="email">{{ query }}</span>:
{{ text "We found an entry for <span class=\"email\">{{ query }}</span>:" rerender }}
</p>
<p>
<a href="{{base_uri}}/vks/v1/by-fingerprint/{{ fpr }}">{{base_uri}}/vks/v1/by-fingerprint/{{ fpr }}</a>
<a href="{{ ../base_uri }}/vks/v1/by-fingerprint/{{ fpr }}">{{ ../base_uri }}/vks/v1/by-fingerprint/{{ fpr }}</a>
</p>
<p>
<strong>Hint:</strong> It's more convenient to use
<span class="brand">keys.openpgp.org</span> from your OpenPGP software.<br />
Take a look at our <a href="/about/usage">usage guide</a> for details.
{{text "<strong>Hint:</strong> It's more convenient to use <span class=\"brand\">keys.openpgp.org</span> from your OpenPGP software.<br /> Take a look at our <a href=\"/about/usage\">usage guide</a> for details." }}
</p>
<div class="debug_link" style="color: transparent;">
<a href="{{base_uri}}/debug?q={{ fpr }}">debug info</a>
<a href="{{ ../base_uri }}/debug?q={{ fpr }}">{{ text "debug info" }}</a>
</div>
</div>
{{/with}}
{{/layout}}

View File

@@ -1,14 +1,31 @@
{{#> layout }}
{{> search-form}}
<p>You can also <a href="/upload">upload</a> or <a href="/manage">manage</a> your key.</p>
{{#with page}}
{{#with error}}
<p><strong>Error</strong>: {{ this }}</p>
{{/with}}
<form action="/search" method="GET">
<div class="search">
<input type="text" class="searchTerm" id="search" name="q" autofocus placeholder="{{ text "Search by Email Address / Key ID / Fingerprint" }}">
<button type="submit" class="searchButton button">
<img src="/assets/search.svg" style="width: 1em; padding-bottom: 4px;"> {{ text "Search" }}
</button>
</div>
</form>
<p>
Find out more <a href="/about">about this service</a>.
{{ text "You can also <a href=\"/upload\">upload</a> or <a href=\"/manage\">manage</a> your key." }}
</p>
<p>
{{ text "Find out more <a href=\"/about\">about this service</a>." }}
</p>
<hr />
<p>
<strong>News:</strong> <a href="/about/news#2019-06-12-launch">Launching a new keyserver! 🚀</a> (2019-06-12)
<strong>{{ text "News:" }}</strong> {{ text "<a href=\"/about/news#2019-11-12-celebrating-100k\">Celebrating 100.000 verified addresses! 📈</a> (2019-11-12)" }}
</p>
{{/with}}
{{/layout}}

View File

@@ -1,27 +1,25 @@
<!doctype html>
<html lang="en">
<html lang="{{lang}}">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="/assets/site.css?v=17" type="text/css"/>
<link rel="stylesheet" href="/assets/site.css?v=19" type="text/css"/>
<link rel="alternate" href="/atom.xml" type="application/atom+xml" title="keys.openpgp.org newsfeed" />
<title>keys.openpgp.org</title>
</head>
<body lang="en">
<body lang="{{lang}}">
<div class="card">
<h1><a class="brand" href="/">keys.openpgp.org</a></h1>
{{#with error }}
<p><strong>Error</strong>: {{ this }}</p>
{{/with}}
{{> @partial-block }}
<div class="spacer"></div>
</div>
<div class="attribution">
<p>
<a href="https://gitlab.com/hagrid-keyserver/hagrid/">Hagrid</a>
v{{ version }} built from
{{ text "v{{ version }} built from" rerender }}
<a href="https://gitlab.com/hagrid-keyserver/hagrid/commit/{{ commit }}">{{ commit }}</a>
</p>
<p>Powered by <a href="https://sequoia-pgp.org">Sequoia-PGP</a></p>
<p>Background image retrieved from <a href="https://www.toptal.com/designers/subtlepatterns/subtle-grey/">Subtle Patterns</a> under CC BY-SA 3.0</p>
<p>{{ text "Powered by <a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>" }}</p>
<p>{{ text "Background image retrieved from <a href=\"https://www.toptal.com/designers/subtlepatterns/subtle-grey/\">Subtle Patterns</a> under CC BY-SA 3.0" }}</p>
</div>
</body>
</html>

View File

@@ -1,8 +1,7 @@
{{#> layout }}
<h2>Maintenance Mode</h2>
<h2>{{ text "Maintenance Mode" }}</h2>
<p>
{{message}}
</p>
{{/layout}}

View File

@@ -1,18 +1,25 @@
{{#> layout }}
<center><h2>Manage your key</h2></center>
{{#with page}}
{{#with error }}
<p><strong>Error</strong>: {{ this }}</p>
{{/with}}
<center><h2>{{ text "Manage your key" }}</h2></center>
<form action="/manage" method="POST">
<div class="manage">
<input type="text" name="search_term" class="manageEmail" autofocus
placeholder="Enter any verified e-mail address of your key">
placeholder="{{ text "Enter any verified email address for your key" }}">
<button type="submit" class="manageButton button">
Send link
{{ text "Send link" }}
</button>
</div>
</form>
<p>
We will send you an e-mail with a link you can use to remove any of your
e-mail addresses from search.
{{ text "We will send you an email with a link you can use to remove any of your email addresses from search." }}
</p>
{{/with}}
{{/layout}}

View File

@@ -1,11 +1,12 @@
{{#> layout }}
{{#with page}}
<p>
Managing the key <span class="fingerprint"><a href="{{key_link}}" target="_blank">{{key_fpr}}</a></span>.
{{ text "Managing the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" target=\"_blank\">{{ key_fpr }}</a></span>." rerender }}
</p>
{{#if uid_status}}
<p style="padding-top: 1em;">
Your key is published with the following identity information:
{{ text "Your key is published with the following identity information:" }}
</p>
{{#each uid_status}}
@@ -14,7 +15,7 @@
<form action="/manage/unpublish" method="post">
<input type="hidden" name="token" value="{{../token}}" />
<input type="hidden" name="address" value="{{address}}" />
<input type="submit" class="link" value="Delete">
<input type="submit" class="link" value="{{ text "Delete" }}">
</form>
</div>
<p>
@@ -24,19 +25,18 @@
{{/each}}
<p style="line-height: 1.8em;">
Clicking "delete" on any address will remove it from this key. It will no longer appear in a search.<br />
To add another address, <a href="/upload">upload</a> the key again.
{{ text "Clicking \"delete\" on any address will remove it from this key. It will no longer appear in a search.<br /> To add another address, <a href=\"/upload\">upload</a> the key again." }}
</p>
{{else}}
<p style="padding-top: 1em;">
Your key is published as only non-identity information.
(<a href="/about" target="_blank">what does this mean?</a>)
{{ text "Your key is published as only non-identity information. (<a href=\"/about\" target=\"_blank\">What does this mean?</a>)" }}
</p>
<p>
To add an address, <a href="/upload">upload</a> the key again.
{{ text "To add an address, <a href=\"/upload\">upload</a> the key again." }}
</p>
{{/if}}
{{/with}}
{{/layout}}

View File

@@ -1,5 +1,7 @@
{{#> layout }}
{{#with page}}
<p>
We have sent an email with further instructions to <span class="email">{{address}}</span>.
{{ text "We have sent an email with further instructions to <span class=\"email\">{{ address }}</span>." rerender }}
</p>
{{/with}}
{{/layout}}

View File

@@ -1,10 +0,0 @@
<div class="row">
<form action="/search" method=GET>
<div class="search">
<input type="text" class="searchTerm" id="search" name="q" autofocus placeholder="Search by Email Address / Key ID / Fingerprint">
<button type="submit" class="searchButton button">
<img src="/assets/search.svg" style="width: 1em; padding-bottom: 4px;"> Search
</button>
</div>
</form>
</div>

View File

@@ -1,20 +1,10 @@
{{#> layout}}
<div class="row">
{{#if verified }}
{{#with page}}
<div id="verification-result">
<p>
Your key
<span class="fingerprint">{{key_fpr}}</span>
is now published
for the identity <a href="{{userid_link}}" target="_blank"><span class="email">{{ userid }}</span></a>.
{{ text "Your key <span class=\"fingerprint\">{{ key_fpr }}</span> is now published for the identity <a href=\"{{userid_link}}\" target=\"_blank\"><span class=\"email\">{{ userid }}</span></a>." rerender }}
</p>
{{else}}
<p>
Verification failed! Perhaps the link you used was expired?
</p>
<p>
You can <a href="/upload">try uploading again</a>.
</p>
{{/if}}
</div>
{{/with}}
{{/layout}}

View File

@@ -1,6 +1,7 @@
{{#> layout }}
{{#with page}}
<p>
Your keys have been successfully uploaded:
{{ text "Your keys have been successfully uploaded:" }}
</p>
<ul>
@@ -10,6 +11,7 @@
</ul>
<p>
<strong>Note:</strong> To make keys searchable by address, you must upload them individually.
{{ text "<strong>Note:</strong> To make keys searchable by email address, you must upload them individually." }}
</p>
{{/with}}
{{/layout}}

View File

@@ -1,47 +1,48 @@
{{#> layout }}
{{#with page}}
<p>
You uploaded the key <span class="fingerprint"><a href="{{key_link}}" target="_blank">{{key_fpr}}</a></span>.
{{ text "You uploaded the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" target=\"_blank\">{{ key_fpr }}</a></span>." rerender }}
</p>
{{#if is_revoked}}
<p>
<strong>This key is revoked.</strong>
It is published without identity information
(<a href="/about" target="_blank">what does this mean?</a>),
and can't be made available for search
by e-mail address.
<strong>{{ text "This key is revoked." }}</strong>
</p>
<p>
{{ text "It is published without identity information and can't be made available for search by email address. (<a href=\"/about\" target=\"_blank\">What does this mean?</a>)" }}
</p>
{{else}}
{{#if email_published}}
<p style="padding-top: 1em;">
This key is now published with the following identity information (<a href="/about" target="_blank">what does this mean?</a>):
{{ text "This key is now published with the following identity information (<a href=\"/about\" target=\"_blank\">what does this mean?</a>):" }}
</p>
{{#each email_published}}
<div class="publishedUid">
<div>Published</div>
<div>{{ text "Published" }}</div>
<p><span class="email">{{this}}</span></p>
</div>
{{/each}}
{{else}}
<p style="padding-top: 1em;">
This key is now published with only non-identity information (<a href="/about" target="_blank">what does this mean?</a>)
{{ text "This key is now published with only non-identity information. (<a href=\"/about\" target=\"_blank\">What does this mean?</a>)" }}
</p>
{{/if}}
{{#if email_unpublished}}
<p style="padding-top: 1em;">
To make the key available for search by e-mail address, you can verify it belongs to you:
{{ text "To make the key available for search by email address, you can verify it belongs to you:" }}
</p>
{{#each email_unpublished}}
<div class="publishedUid">
<div>
{{#if requested}}
Verification Pending
{{ text "Verification Pending" }}
{{else}}
<form action="/upload/request-verify" method="post">
<input type="hidden" name="token" value="{{../token}}" />
<input type="hidden" name="address" value="{{address}}" />
<input type="submit" class="link" value="Send Verification Mail">
<input type="submit" class="link" value="{{ text "Send Verification Email" }}">
</form>
{{/if}}
</div>
@@ -49,27 +50,18 @@
</div>
{{/each}}
<p>
<strong>Note:</strong> Some providers delay e-mails for up to 15 minutes
to prevent spam. Please be patient.
{{ text "<strong>Note:</strong> Some providers delay emails for up to 15 minutes to prevent spam. Please be patient." }}
</p>
{{/if}}
{{#if count_unparsed}}
{{#if count_unparsed_one}}
<p style="padding-top: 1em;">
This key contains one identity that could not be parsed as an email
address.<br />
This identity can't be published
on <span class="brand">keys.openpgp.org</span>.
(<a href="/about/faq#non-email-uids" target="_blank">why?</a>)
{{ text "This key contains one identity that could not be parsed as an email address.<br /> This identity can't be published on <span class=\"brand\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-email-uids\" target=\"_blank\">Why?</a>)" }}
</p>
{{else}}
<p style="padding-top: 1em;">
This key contains {{count_unparsed}} identities that could not be parsed
as an email address.<br />
These identities can't be published
on <span class="brand">keys.openpgp.org</span>.
(<a href="/about/faq#non-email-uids" target="_blank">why?</a>)
{{ text "This key contains {{ count_unparsed }} identities that could not be parsed as an email address.<br /> These identities can't be published on <span class=\"brand\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-email-uids\" target=\"_blank\">Why?</a>)" rerender }}
</p>
{{/if}}
{{/if}}
@@ -77,18 +69,16 @@
{{#if count_revoked}}
{{#if count_revoked_one}}
<p style="padding-top: 1em;">
This key contains one revoked identity, which is not published.
(<a href="/about/faq#revoked-uids" target="_blank">Why?</a>)
{{ text "This key contains one revoked identity, which is not published. (<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Why?</a>)" rerender }}
</p>
{{else}}
<p style="padding-top: 1em;">
This key contains {{count_revoked}} revoked identities, which are not
published.
(<a href="/about/faq#revoked-uids" target="_blank">Why?</a>)
{{ text "This key contains {{ count_revoked }} revoked identities, which are not published. (<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Why?</a>)" rerender }}
</p>
{{/if}}
{{/if}}
{{/if}}
{{/with}}
{{/layout}}

View File

@@ -1,35 +1,26 @@
{{#> layout }}
{{#with page}}
<div class="ui">
<center><h2>Upload your key</h2></center>
<center><h2>{{ text "Upload your key" }}</h2></center>
{{#with error }}
<p><strong>Error</strong>: {{ this }}</p>
{{/with}}
<form action="/upload/submit" method="POST" enctype="multipart/form-data">
<div class="upload">
<input type="file" id="keytext" name="keytext" autofocus class="fileUpload" placeholder="Your public key"/>
<input type="file" id="keytext" name="keytext" autofocus class="fileUpload" placeholder="{{ text "Your public key" }}"/>
<button type="submit" class="uploadButton button">
<img src="/assets/upload.svg" style="width: 1em;"> Upload
<img src="/assets/upload.svg" style="width: 1em;"> {{ text "Upload" }}
</button>
</div>
</form>
<p>
Need more info? Check our <a target="_blank" href="/about">intro</a> and <a target="_blank" href="/about/usage">usage guide</a>!
{{ text "Need more info? Check our <a target=\"_blank\" href=\"/about\">intro</a> and <a target=\"_blank\" href=\"/about/usage\">usage guide</a>." }}
</p>
<script>
var body = document.getElementsByTagName("body")[0];
body.addEventListener('dragover', function(e) {
e.preventDefault();
e.stopPropagation();
}, false);
body.addEventListener('drop', function(e) {
e.preventDefault();
e.stopPropagation();
if (e.dataTransfer) {
document.getElementById('keytext').files = e.dataTransfer.files;
}
}, false);
</script>
<script src="/assets/js/upload.js" async></script>
</div>
{{/with}}
{{/layout}}

View File

@@ -0,0 +1,17 @@
{{#> layout}}
{{#with page}}
<div id="container">
<p>
{{ text "Verifying your email address…" }}
</p>
<p>
<form method="POST" action="/verify/{{token}}" id="postform">
{{ text "If the process doesn't complete after a few seconds, please <input type=\"submit\" class=\"textbutton\" value=\"click here\" />." }}
</form>
</p>
</div>
<script src="/assets/js/upload-verify.js" type="text/javascript"></script>
{{/with}}
{{/layout}}

View File

@@ -5,6 +5,8 @@
client_max_body_size 1m;
client_body_buffer_size 128k;
limit_req_status 429;
# Change all HTTP 502 errors into 500, to avoid being "marked as dead" by GnuPG
# if we ever get a spurious 502 (e.g. during a restart of hagrid).
# See https://gitlab.com/sequoia-pgp/hagrid/issues/94
@@ -28,19 +30,10 @@ location /vks/v1/request-verify {
}
location /vks/v1/ {
location ~ ^/vks/v1/by-email/([^/][^/])([^/][^/])([^/]*)$ {
add_header 'Access-Control-Allow-Origin' '*';
# we have some trouble with uri encoding here. just route through
# hagrid, for now.
proxy_pass http://127.0.0.1:8080;
# error_page 404 /errors/404-by-email.htm;
# default_type application/pgp-keys;
# add_header Content-Disposition 'attachment; filename="$1$2$3.asc"';
# try_files /keys/links/by-email/$1/$2/$3 =404;
}
location ~ ^/vks/v1/by-fingerprint/(?:0x)?([^/][^/])([^/][^/])(..*)$ {
error_page 404 /errors/404-by-fpr.htm;
limit_req zone=search_fpr_keyid burst=30;
error_page 404 /errors-static/404-by-fpr.htm;
default_type application/pgp-keys;
add_header Content-Disposition 'attachment; filename="$1$2$3.asc"';
add_header 'Access-Control-Allow-Origin' '*';
@@ -48,58 +41,134 @@ location /vks/v1/ {
}
location ~ ^/vks/v1/by-keyid/(?:0x)?([^/][^/])([^/][^/])(.*)$ {
error_page 404 /errors/404-by-keyid.htm;
limit_req zone=search_fpr_keyid burst=30;
error_page 429 /errors-static/429-rate-limit-vks-fpr.htm;
error_page 404 /errors-static/404-by-keyid.htm;
default_type application/pgp-keys;
add_header Content-Disposition 'attachment; filename="$1$2$3.asc"';
add_header 'Access-Control-Allow-Origin' '*';
try_files /keys/links/by-keyid/$1/$2/$3 =404;
}
location /vks/v1/by-email/ {
limit_req zone=search_email burst=50 nodelay;
error_page 429 /errors-static/429-rate-limit-vks-email.htm;
set $args "";
add_header 'Access-Control-Allow-Origin' '*';
error_page 400 /errors/400-vks-invalid.htm;
proxy_pass http://127.0.0.1:8080;
# we have some trouble with uri encoding here. just route through
# hagrid, for now.
# error_page 404 /errors-static/404-by-email.htm;
# default_type application/pgp-keys;
# add_header Content-Disposition 'attachment; filename="$1$2$3.asc"';
# try_files /keys/links/by-email/$1/$2/$3 =404;
}
add_header 'Access-Control-Allow-Origin' '*';
error_page 400 /errors-static/400-vks-invalid.htm;
return 400;
}
# Common HKP requests.
location /pks/lookup {
# rewrite this to a path we can match on (right below)
if ($args ~ "op=.*search=") {
# urldecode the search parameter
set_by_lua $arg_search_decoded "return ngx.arg[1]:gsub(\"%%(%x%x)\", function(x) return string.char(tonumber(x, 16)) end)" $arg_search;
# if the search argument contains angle brackets, strip everything around them
set_by_lua $arg_search_decoded "return ngx.arg[1]:gsub(\"^.*<([^>]-)>.*$\", \"%1\")" $arg_search_decoded;
rewrite . /pks/internal/$arg_op/$arg_search_decoded last;
}
add_header 'Access-Control-Allow-Origin' '*';
error_page 400 /errors-static/400-pks-invalid.htm;
return 400;
}
location /pks/internal {
internal;
# search by key id
# sq keyserver get <KEYID>, gpg --receive-keys <KEYID>
if ($args ~ "^op=get&options=mr&search=(?:0x)?([a-fA-F0-9]{16})$") {
location ~ "^/pks/internal/get/(?:0x)?([a-fA-F0-9]{16})$" {
set_by_lua $keyid "return ngx.arg[1]:upper()" $1;
set $args "";
rewrite . /vks/v1/by-keyid/$keyid last;
}
# search by fpr
# gpg --receive-keys <FINGERPRINT>
if ($args ~ "^op=get&options=mr&search=(?:0x)?([a-fA-F0-9]{40})$") {
location ~ "^/pks/internal/get/(?:0x)?([a-fA-F0-9]{40})$" {
set_by_lua $fingerprint "return ngx.arg[1]:upper()" $1;
set $args "";
rewrite . /vks/v1/by-fingerprint/$fingerprint last;
}
# search by email
# gpg --locate-key <EMAIL>
if ($request_uri ~ "^/pks/lookup\?op=get&options=mr&search=([^&]{3,}%40[^&]+)") {
location ~ "^/pks/internal/get/(.+(?:%40|@).+)$" {
set_by_lua $email "return ngx.arg[1]:lower()" $1;
set $args "";
rewrite . /vks/v1/by-email/$email last;
}
# gpg --search '<address@example.org>'
# strip angle brackets - we don't need them, but they cause issues
# with the Rocket framework
# see https://gitlab.com/sequoia-pgp/hagrid/issues/94
if ($request_uri ~ "^/pks/lookup\?(.*search=)[^&<>]*\<(.+)\>(.*)") {
set $left $1;
set $middle $2;
set $right $3;
# index by fingerprint
# gpg --search-keys <FINGEPRINT>
location ~ "^/pks/internal/index/(?:0x)?([a-fA-F0-9]{40})$" {
limit_req zone=search_fpr_keyid burst=30;
limit_req_status 429;
error_page 429 /errors-static/429-rate-limit-pks-index.htm;
set $args "";
rewrite . /pks/lookup?$left$middle$right? break;
add_header 'Access-Control-Allow-Origin' '*';
proxy_pass http://127.0.0.1:8080;
}
# forward to backend, which will like serve via x-accel-redirect
# index by keyid
# gpg --search-keys <KEYID>
location ~ "^/pks/internal/index/(?:0x)?([a-fA-F0-9]{16})$" {
limit_req zone=search_fpr_keyid burst=30;
limit_req_status 429;
error_page 429 /errors-static/429-rate-limit-pks-index.htm;
set $args "";
add_header 'Access-Control-Allow-Origin' '*';
proxy_pass http://127.0.0.1:8080;
}
# index by email
# gpg --search-keys <QUERY>
location ~ ^/pks/internal/index/(.+(?:%40|@).+)$ {
limit_req zone=search_email burst=50 nodelay;
limit_req_status 429;
error_page 429 /errors-static/429-rate-limit-pks-index.htm;
set $args "";
add_header 'Access-Control-Allow-Origin' '*';
proxy_pass http://127.0.0.1:8080;
}
add_header 'Access-Control-Allow-Origin' '*';
error_page 400 /errors-static/400-pks-invalid.htm;
return 400;
}
location /errors-static {
internal;
}
location /errors {
internal;
proxy_pass http://127.0.0.1:8080;
proxy_cache static_cache;
}
location /search {
limit_req zone=search_email burst=50 nodelay;
error_page 429 /errors/429/rate-limit-web;
proxy_pass http://127.0.0.1:8080;
}
location /pks {
proxy_pass http://127.0.0.1:8080;
}
@@ -112,10 +181,6 @@ location /verify {
proxy_pass http://127.0.0.1:8080;
}
location /search {
proxy_pass http://127.0.0.1:8080;
}
location /upload {
proxy_pass http://127.0.0.1:8080;
}
@@ -130,6 +195,12 @@ location = / {
proxy_pass http://127.0.0.1:8080;
}
# cache "about" pages
location = /atom.xml {
proxy_cache static_cache;
proxy_pass http://127.0.0.1:8080;
}
# cache "about" pages
location /about {
proxy_cache static_cache;

View File

@@ -1,12 +1,12 @@
[package]
name = "hagridctl"
version = "0.1.0"
authors = ["Vincent Breitmose <look@my.amazin.horse>"]
authors = ["Vincent Breitmoser <look@my.amazin.horse>"]
[dependencies]
hagrid-database = { path = "../database" }
failure = "0.1.5"
sequoia-openpgp = { version = "0.8", default-features = false }
sequoia-openpgp = { version = "0.9", default-features = false }
multipart = "0"
log = "0"
rand = "0.6"

26
make-translated-templates Executable file
View File

@@ -0,0 +1,26 @@
#!/bin/zsh
for i in templates-untranslated/**/*.hbs; do
local template=${${i#templates-untranslated/}}
local prefix_file=${i:h}/template-prefix
local suffix_file=${i:h}/template-suffix
echo -n "$template: "
echo -n "en "
local dist_path=dist/templates/$template
cat $prefix_file $i $suffix_file >! $dist_path
for translated in templates-translated/*/$template(N); do
local locale=${${translated#templates-translated/}%%/*}
local dist_path=dist/templates/localized/$locale/$template
if [[ ! -d ${dist_path:h} ]]; then
mkdir -p ${dist_path:h}
fi
echo -n "$locale "
# echo "cat $prefix_file $translated $suffix_file >! $dist_path"
cat $prefix_file $translated $suffix_file >! $dist_path
done
echo
done

View File

@@ -1,5 +1,6 @@
# allow 6 requests per min -> one each 10s on avg.
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=6r/m;
limit_req_zone $binary_remote_addr zone=search_email:10m rate=1r/m;
limit_req_zone $binary_remote_addr zone=search_fpr_keyid:10m rate=5r/s;
proxy_cache_path /tmp/nginx_cache use_temp_path=off keys_zone=static_cache:10m;
proxy_cache_valid 200 5m;

461
po/hagrid/de.po Normal file
View File

@@ -0,0 +1,461 @@
# Translators:
# Vincent Breitmoser <look@my.amazin.horse>, 2019
#
msgid ""
msgstr ""
"Project-Id-Version: hagrid\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2019-09-27 18:05+0000\n"
"Last-Translator: Vincent Breitmoser <look@my.amazin.horse>, 2019\n"
"Language-Team: German (https://www.transifex.com/otf/teams/102430/de/)\n"
"Language: de\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: src/mail.rs:107
msgctxt "Subject for verification email"
msgid "Verify {userid} for your key on {domain}"
msgstr "Bestätige {userid} für deinen Schlüssel auf {domain}"
#: src/mail.rs:140
msgctxt "Subject for manage email"
msgid "Manage your key on {domain}"
msgstr "Schlüssel-Verwaltung auf {domain}"
#: src/gettext_strings.rs:4
msgid "Error"
msgstr "Fehler"
#: src/gettext_strings.rs:5
msgid "Looks like something went wrong :("
msgstr "Die Operation hat einen internen Fehler versursacht :("
#: src/gettext_strings.rs:6
msgid "Error message: {{ internal_error }}"
msgstr "<strong>Fehlermeldung:</strong> {{ internal_error }}"
#: src/gettext_strings.rs:7
msgid "There was an error with your request:"
msgstr "Die Anfrage verursachte einen Fehler:"
#: src/gettext_strings.rs:8
msgid "We found an entry for <span class=\"email\">{{ query }}</span>:"
msgstr "Eintrag gefunden für <span class=\"email\">{{ query }}</span>:"
#: src/gettext_strings.rs:9
msgid ""
"<strong>Hint:</strong> It's more convenient to use <span class=\"brand"
"\">keys.openpgp.org</span> from your OpenPGP software.<br /> Take a look at "
"our <a href=\"/about/usage\">usage guide</a> for details."
msgstr ""
"<strong>Tip:</strong> Es ist bequemer, <span class=\"brand\">keys.openpgp."
"org</span> aus OpenPGP-Software heraus zu verwenden. <br />\n"
"Mehr dazu findest du in den <a href=\"/about/usage\">Nutzungshinweisen</a>."
#: src/gettext_strings.rs:10
msgid "debug info"
msgstr "debug info"
#: src/gettext_strings.rs:11
msgid "Search by Email Address / Key ID / Fingerprint"
msgstr "Suche nach Email-Adresse / Schlüssel-ID / Fingerprint"
#: src/gettext_strings.rs:12
msgid "Search"
msgstr "Suchen"
#: src/gettext_strings.rs:13
msgid ""
"You can also <a href=\"/upload\">upload</a> or <a href=\"/manage\">manage</"
"a> your key."
msgstr ""
"Du kannst deinen Schlüssel <a href=\"/upload\">hochladen</a> oder <a href=\"/"
"manage\">verwalten</a>."
#: src/gettext_strings.rs:14
msgid "Find out more <a href=\"/about\">about this service</a>."
msgstr "Erfahre mehr <a href=\"/about\">über diesen Keyserver</a>."
#: src/gettext_strings.rs:15
msgid "News:"
msgstr "News:"
#: src/gettext_strings.rs:16
msgid ""
"<a href=\"/about/news#2019-09-12-three-months-later\">Three months after "
"launch ✨</a> (2019-09-12)"
msgstr ""
"<a href=\"/about/news#2019-09-12-three-months-later\">Three months after "
"launch ✨</a> (12.09.2019) (auf Englisch)"
#: src/gettext_strings.rs:17
msgid "v{{ version }} built from"
msgstr "v{{ version }}, Revision"
#: src/gettext_strings.rs:18
msgid "Powered by <a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>"
msgstr "Powered by <a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>"
#: src/gettext_strings.rs:19
msgid ""
"Background image retrieved from <a href=\"https://www.toptal.com/designers/"
"subtlepatterns/subtle-grey/\">Subtle Patterns</a> under CC BY-SA 3.0"
msgstr ""
"Hintergrund von <a href=\"https://www.toptal.com/designers/subtlepatterns/"
"subtle-grey/\">Subtle Patterns</a> unter CC BY-SA 3.0"
#: src/gettext_strings.rs:20
msgid "Maintenance Mode"
msgstr "Wartungsarbeiten"
#: src/gettext_strings.rs:21
msgid "Manage your key"
msgstr "Schlüssel verwalten"
#: src/gettext_strings.rs:22
msgid "Enter any verified email address for your key"
msgstr "Email-Adresse des zu verwaltenden Schlüssels"
#: src/gettext_strings.rs:23
msgid "Send link"
msgstr "Sende Link"
#: src/gettext_strings.rs:24
msgid ""
"We will send you an email with a link you can use to remove any of your "
"email addresses from search."
msgstr ""
"Du wirst eine Email mit einem Link erhalten, der es erlaubt, Adressen aus "
"der Suche zu entfernen."
#: src/gettext_strings.rs:25
msgid ""
"Managing the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
msgstr ""
"Verwaltung des Schlüssels <span class=\"fingerprint\"><a href="
"\"{{ key_link }}\" target=\"_blank\">{{ key_fpr }}</a></span>."
#: src/gettext_strings.rs:26
msgid "Your key is published with the following identity information:"
msgstr "Dieser Schlüssel ist veröffentlicht mit diesen Identitäten:"
#: src/gettext_strings.rs:27
msgid "Delete"
msgstr "Entfernen"
#: src/gettext_strings.rs:28
msgid ""
"Clicking \"delete\" on any address will remove it from this key. It will no "
"longer appear in a search.<br /> To add another address, <a href=\"/upload"
"\">upload</a> the key again."
msgstr ""
"Veröffentlichte Adressen können hier aus dem Schlüssel und der Suche "
"entfernt werden. <br /> Um eine Adresse hinzuzufügen, muss der Schlüssel <a "
"href=\"/upload\">erneut hochgeladen</a> werden."
#: src/gettext_strings.rs:29
msgid ""
"Your key is published as only non-identity information. (<a href=\"/about\" "
"target=\"_blank\">What does this mean?</a>)"
msgstr ""
"Dieser Schlüssel ist jetzt ohne Identitäts-Informationen veröffentlicht. (<a "
"href=\"/about\" target=\"_blank\">Was heisst das?</a>)"
#: src/gettext_strings.rs:30
msgid "To add an address, <a href=\"/upload\">upload</a> the key again."
msgstr ""
"Um eine Identität hinzuzufügen, lade den Schlüssel <a href=\"/upload"
"\">erneut hoch</a>."
#: src/gettext_strings.rs:31
msgid ""
"We have sent an email with further instructions to <span class=\"email"
"\">{{ address }}</span>."
msgstr ""
"Eine Email mit den nächsten Schritten wurde an <span class=\"email"
"\">{{ address }}</span> gesendet."
#: src/gettext_strings.rs:32
msgid "This address has already been verified."
msgstr "Diese Adresse war bereits bestätigt."
#: src/gettext_strings.rs:33
msgid ""
"Your key <span class=\"fingerprint\">{{ key_fpr }}</span> is now published "
"for the identity <a href=\"{{userid_link}}\" target=\"_blank\"><span class="
"\"email\">{{ userid }}</span></a>."
msgstr ""
"Dein Schlüssel <span class=\"fingerprint\">{{key_fpr}}</span> ist jetzt "
"veröffentlicht mit der Identität <a href=\"{{userid_link}}\" target=\"_blank"
"\"><span class=\"email\">{{ userid }}</span></a>."
#: src/gettext_strings.rs:34
msgid "Upload your key"
msgstr "Schlüssel hochladen"
#: src/gettext_strings.rs:35
msgid "Upload"
msgstr "Upload"
#: src/gettext_strings.rs:36
msgid ""
"Need more info? Check our <a target=\"_blank\" href=\"/about\">intro</a> and "
"<a target=\"_blank\" href=\"/about/usage\">usage guide</a>."
msgstr ""
"Mehr Info? Wirf einen Blick auf unsere <a target=\"_blank\" href=\"/about"
"\">Übersicht</a> und <a target=\"_blank\" href=\"/about/usage"
"\">Nutzungshinweise</a>."
#: src/gettext_strings.rs:37
msgid ""
"You uploaded the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
msgstr ""
"Schlüssel <span class=\"fingerprint\"><a href=\"{{ key_link }}\" target="
"\"_blank\">{{key_fpr}}</a></span> erfolgreich hochgeladen."
#: src/gettext_strings.rs:38
msgid "This key is revoked."
msgstr "Dieser Schlüssel ist widerrufen."
#: src/gettext_strings.rs:39
msgid ""
"It is published without identity information and can't be made available for "
"search by email address (<a href=\"/about\" target=\"_blank\">what does this "
"mean?</a>)."
msgstr ""
"Er wird veröffentlicht ohne Identitäten, ist aber nicht für eine Suche nach "
"Email-Adresse verfügbar. (<a href=\"/about\" target=\"_blank\">Was heisst "
"das?</a>)"
#: src/gettext_strings.rs:40
msgid ""
"This key is now published with the following identity information (<a href="
"\"/about\" target=\"_blank\">what does this mean?</a>):"
msgstr ""
"Dieser Schlüssel ist mit folgenden Identitäten veröffentlicht (<a href=\"/"
"about\" target=\"_blank\">was heisst das?</a>):"
#: src/gettext_strings.rs:41
msgid "Published"
msgstr "Veröffentlicht"
#: src/gettext_strings.rs:42
msgid ""
"This key is now published with only non-identity information. (<a href=\"/"
"about\" target=\"_blank\">What does this mean?</a>)"
msgstr ""
"Dieser Schlüssel ist jetzt ohne Identitäts-Informationen veröffentlicht. (<a "
"href=\"/about\" target=\"_blank\">Was heisst das?</a>)"
#: src/gettext_strings.rs:43
msgid ""
"To make the key available for search by email address, you can verify it "
"belongs to you:"
msgstr ""
"Um den Schlüssel für eine Suche nach Email-Adresse verfügbar zu machen, muss "
"die entsprechende Adresse erst verifiziert werden:"
#: src/gettext_strings.rs:44
msgid "Verification Pending"
msgstr "Bestätigung wird erwartet"
#: src/gettext_strings.rs:45
msgid ""
"<strong>Note:</strong> Some providers delay emails for up to 15 minutes to "
"prevent spam. Please be patient."
msgstr ""
"<strong>Hinweis:</strong> Manche Provider verzögern den Empfang von Emails "
"um bis zu 15 Minuten, um Spam zu verhindern. Bitte einen Moment Geduld."
#: src/gettext_strings.rs:46
msgid "Send Verification Email"
msgstr "Bestätigungs-Email senden"
#: src/gettext_strings.rs:47
msgid ""
"This key contains one identity that could not be parsed as an email address."
"<br /> This identity can't be published on <span class=\"brand\">keys."
"openpgp.org</span>. (<a href=\"/about/faq#non-email-uids\" target=\"_blank"
"\">Why?</a>)"
msgstr ""
"Dieser Schlüssel enthält eine Identität welche nicht als Email Adresse "
"interpretiert werden konnte.<br /> Diese Identität kann nicht auf <span "
"class=\"brand\">keys.openpgp.org</span> veröffentlich werden. (<a href=\"/"
"about/faq#non-email-uids\" target=\"_blank\">Warum?</a>)"
#: src/gettext_strings.rs:48
msgid ""
"This key contains {{ count_unparsed }} identities that could not be parsed "
"as an email address.<br /> These identities can't be published on <span "
"class=\"brand\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-email-"
"uids\" target=\"_blank\">Why?</a>)"
msgstr ""
"Dieser Schlüssel enthält {{ count_unparsed }} Identitäten, die nicht als "
"Email-Adressen erkannt werden konnten.<br /> Diese Identitäten können nicht "
"auf <span class=\"brand\">keys.openpgp.org</span> veröffentlicht werden. (<a "
"href=\"/about/faq#non-email-uids\" target=\"_blank\">Warum?</a>)"
#: src/gettext_strings.rs:49
msgid ""
"This key contains one revoked identity, which is not published. (<a href=\"/"
"about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
msgstr ""
"Dieser Schlüssel enthält eine widerrufene Identität. Diese wird nicht "
"veröffentlicht. (<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Warum?"
"</a>)"
#: src/gettext_strings.rs:50
msgid ""
"This key contains {{ count_revoked }} revoked identities, which are not "
"published. (<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
msgstr ""
"Dieser Schlüssel enthält {{ count_revoked }} widerrufene Identitäten. Diese "
"werden nicht veröffentlicht. (<a href=\"/about/faq#revoked-uids\" target="
"\"_blank\">Warum?</a>)"
#: src/gettext_strings.rs:51
msgid "Your keys have been successfully uploaded:"
msgstr "Schlüssel erfolgreich hochgeladen:"
#: src/gettext_strings.rs:52
msgid ""
"<strong>Note:</strong> To make keys searchable by email address, you must "
"upload them individually."
msgstr ""
"<strong>Hinweis:</strong> Um Schlüssel für die Email-Adresssuche zu "
"bestätigen, müssen sie jeweils einzeln hochgeladen werden."
#: src/gettext_strings.rs:53
msgid "Verifying your email address…"
msgstr "Email-Adresse wird bestätigt..."
#: src/gettext_strings.rs:54
msgid ""
"If the process doesn't complete after a few seconds, please <input type="
"\"submit\" class=\"textbutton\" value=\"click here\" />."
msgstr ""
"Wenn der Vorgang nicht in einigen Sekunden erfolgreich ist, bitte<input type="
"\"submit\" class=\"textbutton\" value=\"hier klicken\"/>."
#: src/gettext_strings.rs:56
msgid "Manage your key on {{domain}}"
msgstr "Schlüssel-Verwaltung auf {{domain}}"
#: src/gettext_strings.rs:58
msgid "Hi,"
msgstr "Hi,"
#: src/gettext_strings.rs:59
msgid ""
"This is an automated message from <a href=\"{{base_uri}}\" style=\"text-"
"decoration:none; color: #333\">{{domain}}</a>."
msgstr ""
"Dies ist eine automatisierte Nachricht von <a href=\"{{base_uri}}\" style="
"\"text-decoration:none; color: #333\">{{ domain }}</a>."
#: src/gettext_strings.rs:60
msgid "If you didn't request this message, please ignore it."
msgstr "Falls dies unerwartet ist, bitte die Nachricht ignorieren."
#: src/gettext_strings.rs:61
msgid "OpenPGP key: <tt>{{primary_fp}}</tt>"
msgstr "OpenPGP Schlüssel: <tt>{{primary_fp}}</tt>"
#: src/gettext_strings.rs:62
msgid ""
"To manage and delete listed addresses on this key, please follow the link "
"below:"
msgstr ""
"Du kannst die Identitäten dieses Schlüssels unter folgendem Link verwalten:"
#: src/gettext_strings.rs:63
msgid ""
"You can find more info at <a href=\"{{base_uri}}/about\">{{domain}}/about</"
"a>."
msgstr ""
"Weiter Informationen findest du unter <a href=\"{{base_uri}}/about"
"\">{{domain}}/about</a>."
#: src/gettext_strings.rs:64
msgid "distributing OpenPGP keys since 2019"
msgstr "Verzeichnis für OpenPGP-Schlüssel seit 2019"
#: src/gettext_strings.rs:67
msgid "This is an automated message from {{domain}}."
msgstr "Dies ist eine automatische Nachricht von {{domain}}."
#: src/gettext_strings.rs:69
msgid "OpenPGP key: {{primary_fp}}"
msgstr "OpenPGP Schlüssel: {{primary_fp}}"
#: src/gettext_strings.rs:71
msgid "You can find more info at {{base_uri}}/about"
msgstr "Weiter Informationen findest du unter {{base_uri}}/about"
#: src/gettext_strings.rs:74
msgid "Verify {{userid}} for your key on {{domain}}"
msgstr "Bestätige {{userid}} für deinen Schlüssel auf {{domain}}"
#: src/gettext_strings.rs:80
msgid ""
"To let others find this key from your email address \"<a rel=\"nofollow\" "
"href=\"#\" style=\"text-decoration:none; color: #333\">{{userid}}</a>\", "
"please click the link below:"
msgstr ""
"Damit der Schlüssel über die Email-Adresse \"<a rel=\"nofollow\" href=\"#\" "
"style=\"text-decoration:none; color: #333\">{{userid}}</a>\" gefunden werden "
"kann, klicke den folgenden Link:"
#: src/gettext_strings.rs:88
msgid ""
"To let others find this key from your email address \"{{userid}}\",\n"
"please follow the link below:"
msgstr ""
"Damit der Schlüssel über die Email-Adresse \"{{userid}}\" gefunden werden "
"kann,\n"
"klicke den folgenden Link:"
#: src/web/manage.rs:103
msgid "This link is invalid or expired"
msgstr "Dieser Link ist ungültig, oder bereits abgelaufen."
#: src/web/manage.rs:129
msgid "Malformed address: {address}"
msgstr "Ungültiges Adress-Format: {address}"
#: src/web/manage.rs:136
msgid "No key for address: {address}"
msgstr "Kein Schlüssel gefunden für {address}"
#: src/web/manage.rs:152
msgid "A request has already been sent for this address recently."
msgstr "Eine E-Mail für diesen Schlüssel wurde erst kürzlich versandt."
#: src/web/vks.rs:112
msgid "Parsing of key data failed."
msgstr "Fehler bei Verarbeitung des Schlüssel-Materials."
#: src/web/vks.rs:121
msgid "Whoops, please don't upload secret keys!"
msgstr "Ups, bitte keine geheimen Schlüssel hochladen!"
#: src/web/vks.rs:134
msgid "No key uploaded."
msgstr "Es wurde kein Schlüssel hochgeladen."
#: src/web/vks.rs:178
msgid "Error processing uploaded key."
msgstr "Fehler bei Verarbeitung des hochgeladenen Schlüssels."
#: src/web/vks.rs:248
msgid "Upload session expired. Please try again."
msgstr "Zeitlimit beim Hochladen abgelaufen. Bitte versuch es erneut."
#: src/web/vks.rs:285
msgid "Invalid verification link."
msgstr "Ungültiger Bestätigungs-Link."

640
po/hagrid/en.po Normal file
View File

@@ -0,0 +1,640 @@
msgid ""
msgstr ""
"Project-Id-Version: hagrid\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2018-06-15 16:33-0700\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: en\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: src/mail.rs:107
#, fuzzy
msgctxt "Subject for verification email"
msgid "Verify {userid} for your key on {domain}"
msgstr "Verify {{userid}} for your key on {{domain}}"
#: src/mail.rs:140
#, fuzzy
msgctxt "Subject for manage email"
msgid "Manage your key on {domain}"
msgstr "Manage your key on {{domain}}"
#: src/gettext_strings.rs:4
msgid "Error"
msgstr "Error"
#: src/gettext_strings.rs:5
msgid "Looks like something went wrong :("
msgstr "Looks like something went wrong :("
#: src/gettext_strings.rs:6
#, fuzzy
msgid "Error message: {{ internal_error }}"
msgstr "<strong>Error:</strong> {{ internal_error }}"
#: src/gettext_strings.rs:7
msgid "There was an error with your request:"
msgstr ""
#: src/gettext_strings.rs:8
#, fuzzy
msgid "We found an entry for <span class=\"email\">{{ query }}</span>:"
msgstr "We found an entry for <span class=\"email\">{{ query }}</span>:"
#: src/gettext_strings.rs:9
msgid ""
"<strong>Hint:</strong> It's more convenient to use <span class=\"brand"
"\">keys.openpgp.org</span> from your OpenPGP software.<br /> Take a look at "
"our <a href=\"/about/usage\">usage guide</a> for details."
msgstr ""
#: src/gettext_strings.rs:10
msgid "debug info"
msgstr "debug info"
#: src/gettext_strings.rs:11
msgid "Search by Email Address / Key ID / Fingerprint"
msgstr "Search by Email Address / Key ID / Fingerprint"
#: src/gettext_strings.rs:12
msgid "Search"
msgstr "Search"
#: src/gettext_strings.rs:13
msgid ""
"You can also <a href=\"/upload\">upload</a> or <a href=\"/manage\">manage</"
"a> your key."
msgstr ""
"You can also <a href=\"/upload\">upload</a> or <a href=\"/manage\">manage</"
"a> your key."
#: src/gettext_strings.rs:14
msgid "Find out more <a href=\"/about\">about this service</a>."
msgstr "Find out more <a href=\"/about\">about this service</a>."
#: src/gettext_strings.rs:15
msgid "News:"
msgstr "News:"
#: src/gettext_strings.rs:16
msgid ""
"<a href=\"/about/news#2019-09-12-three-months-later\">Three months after "
"launch ✨</a> (2019-09-12)"
msgstr ""
#: src/gettext_strings.rs:17
#, fuzzy
msgid "v{{ version }} built from"
msgstr "v{{ version }} built from"
#: src/gettext_strings.rs:18
msgid "Powered by <a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>"
msgstr "Powered by <a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>"
#: src/gettext_strings.rs:19
msgid ""
"Background image retrieved from <a href=\"https://www.toptal.com/designers/"
"subtlepatterns/subtle-grey/\">Subtle Patterns</a> under CC BY-SA 3.0"
msgstr ""
"Background image retrieved from <a href=\"https://www.toptal.com/designers/"
"subtlepatterns/subtle-grey/\">Subtle Patterns</a> under CC BY-SA 3.0"
#: src/gettext_strings.rs:20
msgid "Maintenance Mode"
msgstr "Maintenance Mode"
#: src/gettext_strings.rs:21
msgid "Manage your key"
msgstr "Manage your key"
#: src/gettext_strings.rs:22
#, fuzzy
msgid "Enter any verified email address for your key"
msgstr "Enter any verified e-mail address of your key"
#: src/gettext_strings.rs:23
msgid "Send link"
msgstr "Send link"
#: src/gettext_strings.rs:24
#, fuzzy
msgid ""
"We will send you an email with a link you can use to remove any of your "
"email addresses from search."
msgstr ""
"We will send you an e-mail with a link you can use to remove any of your e-"
"mail addresses from search."
#: src/gettext_strings.rs:25
#, fuzzy
msgid ""
"Managing the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
msgstr ""
"Managing the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
#: src/gettext_strings.rs:26
msgid "Your key is published with the following identity information:"
msgstr "Your key is published with the following identity information:"
#: src/gettext_strings.rs:27
msgid "Delete"
msgstr ""
#: src/gettext_strings.rs:28
msgid ""
"Clicking \"delete\" on any address will remove it from this key. It will no "
"longer appear in a search.<br /> To add another address, <a href=\"/upload"
"\">upload</a> the key again."
msgstr ""
"Clicking \"delete\" on any address will remove it from this key. It will no "
"longer appear in a search.<br /> To add another address, <a href=\"/upload"
"\">upload</a> the key again."
#: src/gettext_strings.rs:29
#, fuzzy
msgid ""
"Your key is published as only non-identity information. (<a href=\"/about\" "
"target=\"_blank\">What does this mean?</a>)"
msgstr ""
"Your key is published as only non-identity information. (<a href=\"/about\" "
"target=\"_blank\">what does this mean?</a>)"
#: src/gettext_strings.rs:30
msgid "To add an address, <a href=\"/upload\">upload</a> the key again."
msgstr "To add an address, <a href=\"/upload\">upload</a> the key again."
#: src/gettext_strings.rs:31
#, fuzzy
msgid ""
"We have sent an email with further instructions to <span class=\"email"
"\">{{ address }}</span>."
msgstr ""
"We have sent an email with further instructions to <span class=\"email"
"\">{{ address }}</span>"
#: src/gettext_strings.rs:32
#, fuzzy
msgid "This address has already been verified."
msgstr "This address was already verified."
#: src/gettext_strings.rs:33
#, fuzzy
msgid ""
"Your key <span class=\"fingerprint\">{{ key_fpr }}</span> is now published "
"for the identity <a href=\"{{userid_link}}\" target=\"_blank\"><span class="
"\"email\">{{ userid }}</span></a>."
msgstr ""
"Your key <span class=\"fingerprint\">{{ key_fpr }}</span> is now published "
"for the identity <a href=\"{{userid_link}}\" target=\"_blank\"><span class="
"\"email\">{{ userid }}</span></a>."
#: src/gettext_strings.rs:34
#, fuzzy
msgid "Upload your key"
msgstr "Manage your key"
#: src/gettext_strings.rs:35
msgid "Upload"
msgstr "Upload"
#: src/gettext_strings.rs:36
#, fuzzy
msgid ""
"Need more info? Check our <a target=\"_blank\" href=\"/about\">intro</a> and "
"<a target=\"_blank\" href=\"/about/usage\">usage guide</a>."
msgstr ""
"Need more info? Check our <a target=\"_blank\" href=\"/about\">intro</a> and "
"<a target=\"_blank\" href=\"/about/usage\">usage guide</a>!"
#: src/gettext_strings.rs:37
#, fuzzy
msgid ""
"You uploaded the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
msgstr ""
"You uploaded the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
#: src/gettext_strings.rs:38
msgid "This key is revoked."
msgstr "This key is revoked."
#: src/gettext_strings.rs:39
#, fuzzy
msgid ""
"It is published without identity information and can't be made available for "
"search by email address (<a href=\"/about\" target=\"_blank\">what does this "
"mean?</a>)."
msgstr ""
"It is published without identity information and can't be made available for "
"search by e-mail address"
#: src/gettext_strings.rs:40
msgid ""
"This key is now published with the following identity information (<a href="
"\"/about\" target=\"_blank\">what does this mean?</a>):"
msgstr ""
"This key is now published with the following identity information (<a href="
"\"/about\" target=\"_blank\">what does this mean?</a>):"
#: src/gettext_strings.rs:41
msgid "Published"
msgstr "Published"
#: src/gettext_strings.rs:42
#, fuzzy
msgid ""
"This key is now published with only non-identity information. (<a href=\"/"
"about\" target=\"_blank\">What does this mean?</a>)"
msgstr ""
"This key is now published with only non-identity information (<a href=\"/"
"about\" target=\"_blank\">what does this mean?</a>)"
#: src/gettext_strings.rs:43
#, fuzzy
msgid ""
"To make the key available for search by email address, you can verify it "
"belongs to you:"
msgstr ""
"To make the key available for search by e-mail address, you can verify it "
"belongs to you:"
#: src/gettext_strings.rs:44
msgid "Verification Pending"
msgstr "Verification Pending"
#: src/gettext_strings.rs:45
msgid ""
"<strong>Note:</strong> Some providers delay emails for up to 15 minutes to "
"prevent spam. Please be patient."
msgstr ""
#: src/gettext_strings.rs:46
#, fuzzy
msgid "Send Verification Email"
msgstr "Send Verification Mail"
#: src/gettext_strings.rs:47
#, fuzzy
msgid ""
"This key contains one identity that could not be parsed as an email address."
"<br /> This identity can't be published on <span class=\"brand\">keys."
"openpgp.org</span>. (<a href=\"/about/faq#non-email-uids\" target=\"_blank"
"\">Why?</a>)"
msgstr ""
"This key contains one identity that could not be parsed as an email address."
"<br /> This identity can't be published on <span class=\"brand\">keys."
"openpgp.org</span>. (<a href=\"/about/faq#non-email-uids\" target=\"_blank"
"\">why?</a>)"
#: src/gettext_strings.rs:48
#, fuzzy
msgid ""
"This key contains {{ count_unparsed }} identities that could not be parsed "
"as an email address.<br /> These identities can't be published on <span "
"class=\"brand\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-email-"
"uids\" target=\"_blank\">Why?</a>)"
msgstr ""
"This key contains {{ count_unparsed }} identities that could not be parsed "
"as an email address.<br /> These identities can't be published on <span "
"class=\"brand\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-email-"
"uids\" target=\"_blank\">why?</a>)"
#: src/gettext_strings.rs:49
#, fuzzy
msgid ""
"This key contains one revoked identity, which is not published. (<a href=\"/"
"about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
msgstr "This key contains one revoked identity, which is not published."
#: src/gettext_strings.rs:50
#, fuzzy
msgid ""
"This key contains {{ count_revoked }} revoked identities, which are not "
"published. (<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
msgstr ""
"This key contains {{ count_revoked }} revoked identities, which are not "
"published."
#: src/gettext_strings.rs:51
msgid "Your keys have been successfully uploaded:"
msgstr "Your keys have been successfully uploaded:"
#: src/gettext_strings.rs:52
#, fuzzy
msgid ""
"<strong>Note:</strong> To make keys searchable by email address, you must "
"upload them individually."
msgstr ""
"<strong>Note:</strong> To make keys searchable by address, you must upload "
"them individually."
#: src/gettext_strings.rs:53
msgid "Verifying your email address…"
msgstr "Verifying your email address…"
#: src/gettext_strings.rs:54
#, fuzzy
msgid ""
"If the process doesn't complete after a few seconds, please <input type="
"\"submit\" class=\"textbutton\" value=\"click here\" />."
msgstr ""
"If the process doesn't complete after a few seconds, <input type=\"submit\" "
"class=\"textbutton\" value=\"click here\" />."
#: src/gettext_strings.rs:56
msgid "Manage your key on {{domain}}"
msgstr "Manage your key on {{domain}}"
#: src/gettext_strings.rs:58
msgid "Hi,"
msgstr "Hi,"
#: src/gettext_strings.rs:59
#, fuzzy
msgid ""
"This is an automated message from <a href=\"{{base_uri}}\" style=\"text-"
"decoration:none; color: #333\">{{domain}}</a>."
msgstr ""
"this is an automated message from <a rel=\"nofollow\" href="
"\"{{ base_uri }}\" style=\"text-decoration:none; color: "
"#333\"><tt>{{ domain }}</tt></a>."
#: src/gettext_strings.rs:60
msgid "If you didn't request this message, please ignore it."
msgstr "If you didn't request this message, please ignore it."
#: src/gettext_strings.rs:61
msgid "OpenPGP key: <tt>{{primary_fp}}</tt>"
msgstr "OpenPGP key: <tt>{{primary_fp}}</tt>"
#: src/gettext_strings.rs:62
msgid ""
"To manage and delete listed addresses on this key, please follow the link "
"below:"
msgstr ""
"To manage and delete listed addresses on this key, please follow the link "
"below:"
#: src/gettext_strings.rs:63
msgid ""
"You can find more info at <a href=\"{{base_uri}}/about\">{{domain}}/about</"
"a>."
msgstr ""
"You can find more info at <a href=\"{{base_uri}}/about\">{{domain}}/about</"
"a>."
#: src/gettext_strings.rs:64
msgid "distributing OpenPGP keys since 2019"
msgstr ""
#: src/gettext_strings.rs:67
#, fuzzy
msgid "This is an automated message from {{domain}}."
msgstr "this is an automated message from {{domain}}. If you didn't"
#: src/gettext_strings.rs:69
msgid "OpenPGP key: {{primary_fp}}"
msgstr "OpenPGP key: {{primary_fp}}"
#: src/gettext_strings.rs:71
msgid "You can find more info at {{base_uri}}/about"
msgstr "You can find more info at {{base_uri}}/about"
#: src/gettext_strings.rs:74
msgid "Verify {{userid}} for your key on {{domain}}"
msgstr "Verify {{userid}} for your key on {{domain}}"
#: src/gettext_strings.rs:80
msgid ""
"To let others find this key from your email address \"<a rel=\"nofollow\" "
"href=\"#\" style=\"text-decoration:none; color: #333\">{{userid}}</a>\", "
"please click the link below:"
msgstr ""
"To let others find this key from your email address \"<a rel=\"nofollow\" "
"href=\"#\" style=\"text-decoration:none; color: #333\">{{userid}}</a>\", "
"please click the link below:"
#: src/gettext_strings.rs:88
#, fuzzy
msgid ""
"To let others find this key from your email address \"{{userid}}\",\n"
"please follow the link below:"
msgstr "To let others find this key from your email address \"{{userid}}\","
#: src/web/manage.rs:103
msgid "This link is invalid or expired"
msgstr ""
#: src/web/manage.rs:129
msgid "Malformed address: {address}"
msgstr ""
#: src/web/manage.rs:136
msgid "No key for address: {address}"
msgstr ""
#: src/web/manage.rs:152
msgid "A request has already been sent for this address recently."
msgstr ""
#: src/web/vks.rs:112
msgid "Parsing of key data failed."
msgstr ""
#: src/web/vks.rs:121
msgid "Whoops, please don't upload secret keys!"
msgstr ""
#: src/web/vks.rs:134
#, fuzzy
msgid "No key uploaded."
msgstr "Your key upload on {}"
#: src/web/vks.rs:178
msgid "Error processing uploaded key."
msgstr ""
#: src/web/vks.rs:248
msgid "Upload session expired. Please try again."
msgstr ""
#: src/web/vks.rs:285
#, fuzzy
msgid "Invalid verification link."
msgstr "Send Verification Mail"
#, fuzzy
#~ msgctxt "Subject for welcome email"
#~ msgid "Your key upload on {domain}"
#~ msgstr "Your key upload on {{domain}}"
#~ msgid "Your key upload on {{domain}}"
#~ msgstr "Your key upload on {{domain}}"
#, fuzzy
#~ msgid "If you didn't upload your key there, please ignore this message."
#~ msgstr ""
#~ "this is an automated message from {{domain}}. If you didn't upload your "
#~ "key"
#, fuzzy
#~ msgid ""
#~ "This key was just uploaded for the first time, and is now published "
#~ "without identity information. If you want to allow others to find this "
#~ "key by e-mail address, please follow this link:"
#~ msgstr ""
#~ "This key was just uploaded for the first time, and is now published "
#~ "without identity information. If you want to allow others to find this "
#~ "key by e-mail address, please follow this link:"
#, fuzzy
#~ msgid ""
#~ "This key was just uploaded for the first time, and is now published "
#~ "without\n"
#~ "identity information. If you want to allow others to find this key by e-"
#~ "mail\n"
#~ "address, please follow this link:"
#~ msgstr ""
#~ "This key was just uploaded for the first time, and is now published "
#~ "without identity information. If you want to allow others to find this "
#~ "key by e-mail address, please follow this link:"
#, fuzzy
#~ msgid ""
#~ "To manage and delete listed addresses on this key, please follow\n"
#~ "the link below:"
#~ msgstr ""
#~ "To manage and delete listed addresses on this key, please follow the link "
#~ "below:"
#, fuzzy
#~ msgid ""
#~ "this is an automated message from {{domain}}. If you didn't upload your "
#~ "key\n"
#~ "there, please ignore this message."
#~ msgstr ""
#~ "this is an automated message from {{domain}}. If you didn't upload your "
#~ "key"
#, fuzzy
#~ msgid ""
#~ "this is an automated message from {{domain}}. If you didn't\n"
#~ "request this message, please ignore it."
#~ msgstr ""
#~ "this is an automated message from {{domain}}. If you didn't upload your "
#~ "key"
#~ msgid "OpenPGP key: <tt>{{ primary_fp }}</tt>"
#~ msgstr "OpenPGP key: <tt>{{ primary_fp }}</tt>"
#~ msgid ""
#~ "You can find more info at <a href=\"{{ base_uri }}/about\">{{ domain }}/"
#~ "about</a>."
#~ msgstr ""
#~ "You can find more info at <a href=\"{{ base_uri }}/about\">{{ domain }}/"
#~ "about</a>."
#~ msgid ""
#~ "this is an automated message from <a rel=\"nofollow\" href="
#~ "\"{{base_uri}}\" style=\"text-decoration:none; color: "
#~ "#333\"><tt>{{domain}}</tt></a>. If you didn't request this message, "
#~ "please ignore it."
#~ msgstr ""
#~ "this is an automated message from <a rel=\"nofollow\" href="
#~ "\"{{base_uri}}\" style=\"text-decoration:none; color: "
#~ "#333\"><tt>{{domain}}</tt></a>. If you didn't request this message, "
#~ "please ignore it."
#~ msgid ""
#~ "this is an automated message from <a rel=\"nofollow\" href="
#~ "\"{{base_uri}}\" style=\"text-decoration:none; color: "
#~ "#333\"><tt>{{domain}}</tt></a>. If you didn't upload your key there, "
#~ "please ignore this message."
#~ msgstr ""
#~ "this is an automated message from <a rel=\"nofollow\" href="
#~ "\"{{base_uri}}\" style=\"text-decoration:none; color: "
#~ "#333\"><tt>{{domain}}</tt></a>. If you didn't upload your key there, "
#~ "please ignore this message."
#~ msgid ""
#~ "Greetings from the <a rel=\"nofollow\" href=\"{{ base_uri }}\" style="
#~ "\"text-decoration:none; color: #333\"><tt>keys.openpgp.org</tt></a> team"
#~ msgstr ""
#~ "Greetings from the <a rel=\"nofollow\" href=\"{{ base_uri }}\" style="
#~ "\"text-decoration:none; color: #333\"><tt>keys.openpgp.org</tt></a> team"
#~ msgid "Greetings from the keys.openpgp.org team"
#~ msgstr "Greetings from the keys.openpgp.org team"
#~ msgid ""
#~ "Greetings from the <a rel=\"nofollow\" href=\"{{base_uri}}\" style=\"text-"
#~ "decoration:none; color: #333\"><tt>keys.openpgp.org</tt></a> team"
#~ msgstr ""
#~ "Greetings from the <a rel=\"nofollow\" href=\"{{base_uri}}\" style=\"text-"
#~ "decoration:none; color: #333\"><tt>keys.openpgp.org</tt></a> team"
#~ msgid "Verification failed! Perhaps the link you used was expired?"
#~ msgstr "Verification failed! Perhaps the link you used was expired?"
#~ msgid "Your public key"
#~ msgstr "Your public key"
#~ msgid "You can <a href=\"/upload\">try uploading again</a>."
#~ msgstr "You can <a href=\"/upload\">try uploading again</a>."
#, fuzzy
#~ msgctxt "Subject for verification email"
#~ msgid "Verify {email} for your key on {domain}"
#~ msgstr "Verify {{userid}} for your key on {{domain}}"
#~ msgid "what does this mean?"
#~ msgstr "what does this mean?"
#~ msgid "Verify {} for your key on {}"
#~ msgstr "Verify {} for your key on {}"
#~ msgid "Manage your key on {}"
#~ msgstr "Manage your key on {}"
#~ msgid "(<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
#~ msgstr "(<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
#~ msgid "request this message, please ignore it."
#~ msgstr "request this message, please ignore it."
#~ msgid "please follow the link below:"
#~ msgstr "please follow the link below:"
#~ msgid "there, please ignore this message."
#~ msgstr "there, please ignore this message."
#~ msgid ""
#~ "This key was just uploaded for the first time, and is now published "
#~ "without"
#~ msgstr ""
#~ "This key was just uploaded for the first time, and is now published "
#~ "without"
#~ msgid ""
#~ "identity information. If you want to allow others to find this key by e-"
#~ "mail"
#~ msgstr ""
#~ "identity information. If you want to allow others to find this key by e-"
#~ "mail"
#~ msgid "address, please follow this link:"
#~ msgstr "address, please follow this link:"
#~ msgid "To manage and delete listed addresses on this key, please follow"
#~ msgstr "To manage and delete listed addresses on this key, please follow"
#~ msgid "the link below:"
#~ msgstr "the link below:"

468
po/hagrid/fr.po Normal file
View File

@@ -0,0 +1,468 @@
# Translators:
# AO <ao@localizationlab.org>, 2019
#
msgid ""
msgstr ""
"Project-Id-Version: hagrid\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2019-09-27 18:05+0000\n"
"Last-Translator: AO <ao@localizationlab.org>, 2019\n"
"Language-Team: French (https://www.transifex.com/otf/teams/102430/fr/)\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: src/mail.rs:107
msgctxt "Subject for verification email"
msgid "Verify {userid} for your key on {domain}"
msgstr "Confirmer {userid} pour votre clé sur {domain}"
#: src/mail.rs:140
msgctxt "Subject for manage email"
msgid "Manage your key on {domain}"
msgstr "Gérer votre clé sur {domain}"
#: src/gettext_strings.rs:4
msgid "Error"
msgstr "Erreur"
#: src/gettext_strings.rs:5
msgid "Looks like something went wrong :("
msgstr "Il semble quun problème est survenu :("
#: src/gettext_strings.rs:6
msgid "Error message: {{ internal_error }}"
msgstr "Message derreur : {{ internal_error }}"
#: src/gettext_strings.rs:7
msgid "There was an error with your request:"
msgstr "Votre demande a généré une erreur :"
#: src/gettext_strings.rs:8
msgid "We found an entry for <span class=\"email\">{{ query }}</span>:"
msgstr ""
"Nous avons trouvé une entrée pour <span class=\"email\">{{ query }}</span> :"
#: src/gettext_strings.rs:9
msgid ""
"<strong>Hint:</strong> It's more convenient to use <span class=\"brand"
"\">keys.openpgp.org</span> from your OpenPGP software.<br /> Take a look at "
"our <a href=\"/about/usage\">usage guide</a> for details."
msgstr ""
"<strong>Conseil :</strong> Il est plus pratique dutiliser <span class="
"\"brand\">keys.openpgp.org</span> à partir de votre logiciel OpenPGP.<br /> "
"Vous trouverez plus de précisions dans notre <a href=\"/about/usage\">guide "
"dutilisation</a>."
#: src/gettext_strings.rs:10
msgid "debug info"
msgstr "renseignements de débogage"
#: src/gettext_strings.rs:11
msgid "Search by Email Address / Key ID / Fingerprint"
msgstr "Cherchez par adresse courriel / ID de clé / empreinte"
#: src/gettext_strings.rs:12
msgid "Search"
msgstr "Chercher"
#: src/gettext_strings.rs:13
msgid ""
"You can also <a href=\"/upload\">upload</a> or <a href=\"/manage\">manage</"
"a> your key."
msgstr ""
"Vous pouvez aussi <a href=\"/upload\">téléverser</a> ou <a href=\"/manage"
"\">gérer</a> votre clé."
#: src/gettext_strings.rs:14
msgid "Find out more <a href=\"/about\">about this service</a>."
msgstr "En apprendre davantage <a href=\"/about\">sur ce service</a>."
#: src/gettext_strings.rs:15
msgid "News:"
msgstr "Nouvelles :"
#: src/gettext_strings.rs:16
msgid ""
"<a href=\"/about/news#2019-09-12-three-months-later\">Three months after "
"launch ✨</a> (2019-09-12)"
msgstr ""
"<a href=\"/about/news#2019-09-12-three-months-later\">Trois mois après le "
"lancement ✨</a> (12-09-2019) (page en anglais)"
#: src/gettext_strings.rs:17
msgid "v{{ version }} built from"
msgstr "v{{ version }} compilée à partir de"
#: src/gettext_strings.rs:18
msgid "Powered by <a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>"
msgstr ""
"Propulsé par <a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a> (site en "
"anglais)"
#: src/gettext_strings.rs:19
msgid ""
"Background image retrieved from <a href=\"https://www.toptal.com/designers/"
"subtlepatterns/subtle-grey/\">Subtle Patterns</a> under CC BY-SA 3.0"
msgstr ""
"Limage darrière-plan a été obtenue de <a href=\"https://www.toptal.com/"
"designers/subtlepatterns/subtle-grey/\">Subtle Patterns</a> sous licence CC "
"BY-SA 3.0"
#: src/gettext_strings.rs:20
msgid "Maintenance Mode"
msgstr "Mode de maintenance"
#: src/gettext_strings.rs:21
msgid "Manage your key"
msgstr "Gérer votre clé"
#: src/gettext_strings.rs:22
msgid "Enter any verified email address for your key"
msgstr "Saisissez une adresse courriel confirmée pour votre clé"
#: src/gettext_strings.rs:23
msgid "Send link"
msgstr "Envoyer le lien"
#: src/gettext_strings.rs:24
msgid ""
"We will send you an email with a link you can use to remove any of your "
"email addresses from search."
msgstr ""
"Nous vous enverrons un courriel avec un lien que vous pourrez utiliser pour "
"supprimer de la recherche lune de vos adresses courriel."
#: src/gettext_strings.rs:25
msgid ""
"Managing the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
msgstr ""
"Gestion de la clé <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
#: src/gettext_strings.rs:26
msgid "Your key is published with the following identity information:"
msgstr "Votre clé est publiée avec les renseignements didentité suivants :"
#: src/gettext_strings.rs:27
msgid "Delete"
msgstr "Supprimer"
#: src/gettext_strings.rs:28
msgid ""
"Clicking \"delete\" on any address will remove it from this key. It will no "
"longer appear in a search.<br /> To add another address, <a href=\"/upload"
"\">upload</a> the key again."
msgstr ""
"Cliquer sur « Supprimer » pour lune des adresses lenlèvera de cette clé. "
"Elle napparaîtra plus lors dune recherche.<br /> Pour ajouter une autre "
"adresse, <a href=\"/upload\">téléversez</a> la clé de nouveau."
#: src/gettext_strings.rs:29
msgid ""
"Your key is published as only non-identity information. (<a href=\"/about\" "
"target=\"_blank\">What does this mean?</a>)"
msgstr ""
"Votre clé nest publiée quen tant que renseignement qui ne permet pas de "
"vous identifier. (<a href=\"/about\" target=\"_blank\">Quest-ce que cela "
"signifie?</a>)"
#: src/gettext_strings.rs:30
msgid "To add an address, <a href=\"/upload\">upload</a> the key again."
msgstr ""
"Pour ajouter une adresse, <a href=\"/upload\">téléversez</a> la clé de "
"nouveau."
#: src/gettext_strings.rs:31
msgid ""
"We have sent an email with further instructions to <span class=\"email"
"\">{{ address }}</span>."
msgstr ""
"Un courriel avec de plus amples instructions a été envoyé à <span class="
"\"email\">{{ address }}</span>."
#: src/gettext_strings.rs:32
msgid "This address has already been verified."
msgstr "Cette adresse a déjà été confirmée."
#: src/gettext_strings.rs:33
msgid ""
"Your key <span class=\"fingerprint\">{{ key_fpr }}</span> is now published "
"for the identity <a href=\"{{userid_link}}\" target=\"_blank\"><span class="
"\"email\">{{ userid }}</span></a>."
msgstr ""
"Votre clé <span class=\"fingerprint\">{{ key_fpr }}</span> est maintenant "
"publiée pour lidentité <a href=\"{{userid_link}}\" target=\"_blank\"><span "
"class=\"email\">{{ userid }}</span></a>. "
#: src/gettext_strings.rs:34
msgid "Upload your key"
msgstr "Téléverser votre clé"
#: src/gettext_strings.rs:35
msgid "Upload"
msgstr "Téléverser"
#: src/gettext_strings.rs:36
msgid ""
"Need more info? Check our <a target=\"_blank\" href=\"/about\">intro</a> and "
"<a target=\"_blank\" href=\"/about/usage\">usage guide</a>."
msgstr ""
"Besoin de plus de précisions? Consultez notre <a target=\"_blank\" href=\"/"
"about\">présentation</a> et notre <a target=\"_blank\" href=\"/about/usage"
"\">guide dutilisation</a>."
#: src/gettext_strings.rs:37
msgid ""
"You uploaded the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
msgstr ""
"Vous avez téléversé la clé <span class=\"fingerprint\"><a href="
"\"{{ key_link }}\" target=\"_blank\">{{ key_fpr }}</a></span>."
#: src/gettext_strings.rs:38
msgid "This key is revoked."
msgstr "Cette clé est révoquée."
#: src/gettext_strings.rs:39
msgid ""
"It is published without identity information and can't be made available for "
"search by email address (<a href=\"/about\" target=\"_blank\">what does this "
"mean?</a>)."
msgstr ""
"Elle est publiée sans renseignements didentité et une recherche par adresse "
"courriel ne la trouvera pas (<a href=\"/about\" target=\"_blank\">quest-ce "
"que cela signifie?</a>)."
#: src/gettext_strings.rs:40
msgid ""
"This key is now published with the following identity information (<a href="
"\"/about\" target=\"_blank\">what does this mean?</a>):"
msgstr ""
"Cette clé est maintenant publiée avec les renseignements didentité suivants "
"(<a href=\"/about\" target=\"_blank\">quest-ce que cela signifie?</a>) :"
#: src/gettext_strings.rs:41
msgid "Published"
msgstr "Publiée"
#: src/gettext_strings.rs:42
msgid ""
"This key is now published with only non-identity information. (<a href=\"/"
"about\" target=\"_blank\">What does this mean?</a>)"
msgstr ""
"Cette clé est maintenant publiée avec seulement des renseignements qui ne "
"permettent pas de vous identifier. (<a href=\"/about\" target=\"_blank"
"\">Quest-ce que cela signifie?</a>)"
#: src/gettext_strings.rs:43
msgid ""
"To make the key available for search by email address, you can verify it "
"belongs to you:"
msgstr ""
"Afin quune recherche par adresse courriel trouve cette clé, vous pouvez "
"confirmer quelle vous appartient :"
#: src/gettext_strings.rs:44
msgid "Verification Pending"
msgstr "La confirmation est en attente"
#: src/gettext_strings.rs:45
msgid ""
"<strong>Note:</strong> Some providers delay emails for up to 15 minutes to "
"prevent spam. Please be patient."
msgstr ""
"<strong>Note :</strong> Certains fournisseurs retardent les courriels "
"jusquà 15 minutes afin de prévenir les courriels indésirables (pourriels). "
"Veuillez faire preuve de patience."
#: src/gettext_strings.rs:46
msgid "Send Verification Email"
msgstr "Envoyer un courriel de confirmation"
#: src/gettext_strings.rs:47
msgid ""
"This key contains one identity that could not be parsed as an email address."
"<br /> This identity can't be published on <span class=\"brand\">keys."
"openpgp.org</span>. (<a href=\"/about/faq#non-email-uids\" target=\"_blank"
"\">Why?</a>)"
msgstr ""
"Cette clé comprend une identité qui na pas pu être analysée en tant "
"quadresse courriel.<br /> Cette identité ne peut pas être publiée sur <span "
"class=\"brand\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-email-uids"
"\" target=\"_blank\">Pourquoi?</a>) "
#: src/gettext_strings.rs:48
msgid ""
"This key contains {{ count_unparsed }} identities that could not be parsed "
"as an email address.<br /> These identities can't be published on <span "
"class=\"brand\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-email-"
"uids\" target=\"_blank\">Why?</a>)"
msgstr ""
"Cette clé comprend {{ count_unparsed }} identités qui nont pas pu être "
"analysées en tant quadresse courriel.<br /> Ces identités ne peuvent pas "
"être publiées sur <span class=\"brand\">keys.openpgp.org</span>. (<a href=\"/"
"about/faq#non-email-uids\" target=\"_blank\">Pourquoi?</a>) "
#: src/gettext_strings.rs:49
msgid ""
"This key contains one revoked identity, which is not published. (<a href=\"/"
"about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
msgstr ""
"Cette clé comprend une identité révoquée qui nest pas publiée. (<a href=\"/"
"about/faq#revoked-uids\" target=\"_blank\">Pourquoi?</a>)"
#: src/gettext_strings.rs:50
msgid ""
"This key contains {{ count_revoked }} revoked identities, which are not "
"published. (<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
msgstr ""
"Cette clé comprend {{ count_revoked }} identités révoquées qui ne sont pas "
"publiées. (<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Pourquoi?</"
"a>)"
#: src/gettext_strings.rs:51
msgid "Your keys have been successfully uploaded:"
msgstr "Vos clés ont été téléversées avec succès :"
#: src/gettext_strings.rs:52
msgid ""
"<strong>Note:</strong> To make keys searchable by email address, you must "
"upload them individually."
msgstr ""
"<strong>Note :</strong> Afin quune recherche par adresse courriel trouve "
"des clés, vous devez les téléverser individuellement."
#: src/gettext_strings.rs:53
msgid "Verifying your email address…"
msgstr "Confirmation de votre adresse courriel…"
#: src/gettext_strings.rs:54
msgid ""
"If the process doesn't complete after a few seconds, please <input type="
"\"submit\" class=\"textbutton\" value=\"click here\" />."
msgstr ""
"Si le processus nest pas terminé après quelques secondes, veuillez <input "
"type=\"submit\" class=\"textbutton\" value=\"cliquer ici\"/>."
#: src/gettext_strings.rs:56
msgid "Manage your key on {{domain}}"
msgstr "Gérer votre clé sur {{domain}}"
#: src/gettext_strings.rs:58
msgid "Hi,"
msgstr "Bonjour,"
#: src/gettext_strings.rs:59
msgid ""
"This is an automated message from <a href=\"{{base_uri}}\" style=\"text-"
"decoration:none; color: #333\">{{domain}}</a>."
msgstr ""
"Ceci est un courriel automatisé de <a href=\"{{base_uri}}\" style=\"text-"
"decoration:none; color: #333\">{{domain}}</a>."
#: src/gettext_strings.rs:60
msgid "If you didn't request this message, please ignore it."
msgstr "Si vous navez pas demandé ce courriel, veuillez lignorer."
#: src/gettext_strings.rs:61
msgid "OpenPGP key: <tt>{{primary_fp}}</tt>"
msgstr "Clé OpenPGP : <tt>{{primary_fp}}</tt>"
#: src/gettext_strings.rs:62
msgid ""
"To manage and delete listed addresses on this key, please follow the link "
"below:"
msgstr ""
"Pour gérer et supprimer les adresses répertoriées de cette clé, veuillez "
"suivre le lien ci-dessous :"
#: src/gettext_strings.rs:63
msgid ""
"You can find more info at <a href=\"{{base_uri}}/about\">{{domain}}/about</"
"a>."
msgstr ""
"Pour de plus amples renseignements, consultez <a href=\"{{base_uri}}/about"
"\">{{domain}}/about</a>."
#: src/gettext_strings.rs:64
msgid "distributing OpenPGP keys since 2019"
msgstr "distribue des clés OpenPGP depuis 2019"
#: src/gettext_strings.rs:67
msgid "This is an automated message from {{domain}}."
msgstr "Ceci est un courriel automatisé de {{domain}}."
#: src/gettext_strings.rs:69
msgid "OpenPGP key: {{primary_fp}}"
msgstr "Clé OpenPGP : {{primary_fp}}"
#: src/gettext_strings.rs:71
msgid "You can find more info at {{base_uri}}/about"
msgstr "Pour de plus amples renseignements, consultez {{base_uri}}/about"
#: src/gettext_strings.rs:74
msgid "Verify {{userid}} for your key on {{domain}}"
msgstr "Confirmer {{userid}} pour votre clé sur {{domain}}"
#: src/gettext_strings.rs:80
msgid ""
"To let others find this key from your email address \"<a rel=\"nofollow\" "
"href=\"#\" style=\"text-decoration:none; color: #333\">{{userid}}</a>\", "
"please click the link below:"
msgstr ""
"Afin de permettre à dautres de trouver cette clé à partir de votre adresse "
"courriel « <a rel=\"nofollow\" href=\"#\" style=\"text-decoration:none; "
"color : #333\">{{userid}}</a> », veuillez cliquer sur le lien ci-dessous :"
#: src/gettext_strings.rs:88
msgid ""
"To let others find this key from your email address \"{{userid}}\",\n"
"please follow the link below:"
msgstr ""
"Afin de permettre à dautres de trouver cette clé à partir de votre\n"
"adresse courriel « {{userid}} », veuillez suivre le lien ci-dessous :"
#: src/web/manage.rs:103
msgid "This link is invalid or expired"
msgstr "Ce lien est invalide ou expiré"
#: src/web/manage.rs:129
msgid "Malformed address: {address}"
msgstr "Cette adresse est malformée :"
#: src/web/manage.rs:136
msgid "No key for address: {address}"
msgstr "Il ny a pas de clé pour cette adresse : {address}"
#: src/web/manage.rs:152
msgid "A request has already been sent for this address recently."
msgstr "Une demande a déjà été envoyée récemment pour cette adresse."
#: src/web/vks.rs:112
msgid "Parsing of key data failed."
msgstr "Échec danalyse des données de la clé."
#: src/web/vks.rs:121
msgid "Whoops, please don't upload secret keys!"
msgstr "Attention : Veuillez ne pas téléverser de clés secrètes!"
#: src/web/vks.rs:134
msgid "No key uploaded."
msgstr "Aucune clé na été téléversée."
#: src/web/vks.rs:178
msgid "Error processing uploaded key."
msgstr "Erreur de traitement de la clé téléversée."
#: src/web/vks.rs:248
msgid "Upload session expired. Please try again."
msgstr "La session de téléversement est expirée. Veuillez ressayer."
#: src/web/vks.rs:285
msgid "Invalid verification link."
msgstr "Le lien de confirmation est invalide."

323
po/hagrid/hagrid.pot Normal file
View File

@@ -0,0 +1,323 @@
msgid ""
msgstr ""
"Project-Id-Version: hagrid\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
#: src/mail.rs:107
msgctxt "Subject for verification email"
msgid "Verify {userid} for your key on {domain}"
msgstr ""
#: src/mail.rs:140
msgctxt "Subject for manage email"
msgid "Manage your key on {domain}"
msgstr ""
#: src/gettext_strings.rs:4
msgid "Error"
msgstr ""
#: src/gettext_strings.rs:5
msgid "Looks like something went wrong :("
msgstr ""
#: src/gettext_strings.rs:6
msgid "Error message: {{ internal_error }}"
msgstr ""
#: src/gettext_strings.rs:7
msgid "There was an error with your request:"
msgstr ""
#: src/gettext_strings.rs:8
msgid "We found an entry for <span class=\"email\">{{ query }}</span>:"
msgstr ""
#: src/gettext_strings.rs:9
msgid "<strong>Hint:</strong> It's more convenient to use <span class=\"brand\">keys.openpgp.org</span> from your OpenPGP software.<br /> Take a look at our <a href=\"/about/usage\">usage guide</a> for details."
msgstr ""
#: src/gettext_strings.rs:10
msgid "debug info"
msgstr ""
#: src/gettext_strings.rs:11
msgid "Search by Email Address / Key ID / Fingerprint"
msgstr ""
#: src/gettext_strings.rs:12
msgid "Search"
msgstr ""
#: src/gettext_strings.rs:13
msgid "You can also <a href=\"/upload\">upload</a> or <a href=\"/manage\">manage</a> your key."
msgstr ""
#: src/gettext_strings.rs:14
msgid "Find out more <a href=\"/about\">about this service</a>."
msgstr ""
#: src/gettext_strings.rs:15
msgid "News:"
msgstr ""
#: src/gettext_strings.rs:16
msgid "<a href=\"/about/news#2019-09-12-three-months-later\">Three months after launch ✨</a> (2019-09-12)"
msgstr ""
#: src/gettext_strings.rs:17
msgid "v{{ version }} built from"
msgstr ""
#: src/gettext_strings.rs:18
msgid "Powered by <a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>"
msgstr ""
#: src/gettext_strings.rs:19
msgid "Background image retrieved from <a href=\"https://www.toptal.com/designers/subtlepatterns/subtle-grey/\">Subtle Patterns</a> under CC BY-SA 3.0"
msgstr ""
#: src/gettext_strings.rs:20
msgid "Maintenance Mode"
msgstr ""
#: src/gettext_strings.rs:21
msgid "Manage your key"
msgstr ""
#: src/gettext_strings.rs:22
msgid "Enter any verified email address for your key"
msgstr ""
#: src/gettext_strings.rs:23
msgid "Send link"
msgstr ""
#: src/gettext_strings.rs:24
msgid "We will send you an email with a link you can use to remove any of your email addresses from search."
msgstr ""
#: src/gettext_strings.rs:25
msgid "Managing the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" target=\"_blank\">{{ key_fpr }}</a></span>."
msgstr ""
#: src/gettext_strings.rs:26
msgid "Your key is published with the following identity information:"
msgstr ""
#: src/gettext_strings.rs:27
msgid "Delete"
msgstr ""
#: src/gettext_strings.rs:28
msgid "Clicking \"delete\" on any address will remove it from this key. It will no longer appear in a search.<br /> To add another address, <a href=\"/upload\">upload</a> the key again."
msgstr ""
#: src/gettext_strings.rs:29
msgid "Your key is published as only non-identity information. (<a href=\"/about\" target=\"_blank\">What does this mean?</a>)"
msgstr ""
#: src/gettext_strings.rs:30
msgid "To add an address, <a href=\"/upload\">upload</a> the key again."
msgstr ""
#: src/gettext_strings.rs:31
msgid "We have sent an email with further instructions to <span class=\"email\">{{ address }}</span>."
msgstr ""
#: src/gettext_strings.rs:32
msgid "This address has already been verified."
msgstr ""
#: src/gettext_strings.rs:33
msgid "Your key <span class=\"fingerprint\">{{ key_fpr }}</span> is now published for the identity <a href=\"{{userid_link}}\" target=\"_blank\"><span class=\"email\">{{ userid }}</span></a>."
msgstr ""
#: src/gettext_strings.rs:34
msgid "Upload your key"
msgstr ""
#: src/gettext_strings.rs:35
msgid "Upload"
msgstr ""
#: src/gettext_strings.rs:36
msgid "Need more info? Check our <a target=\"_blank\" href=\"/about\">intro</a> and <a target=\"_blank\" href=\"/about/usage\">usage guide</a>."
msgstr ""
#: src/gettext_strings.rs:37
msgid "You uploaded the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" target=\"_blank\">{{ key_fpr }}</a></span>."
msgstr ""
#: src/gettext_strings.rs:38
msgid "This key is revoked."
msgstr ""
#: src/gettext_strings.rs:39
msgid "It is published without identity information and can't be made available for search by email address (<a href=\"/about\" target=\"_blank\">what does this mean?</a>)."
msgstr ""
#: src/gettext_strings.rs:40
msgid "This key is now published with the following identity information (<a href=\"/about\" target=\"_blank\">what does this mean?</a>):"
msgstr ""
#: src/gettext_strings.rs:41
msgid "Published"
msgstr ""
#: src/gettext_strings.rs:42
msgid "This key is now published with only non-identity information. (<a href=\"/about\" target=\"_blank\">What does this mean?</a>)"
msgstr ""
#: src/gettext_strings.rs:43
msgid "To make the key available for search by email address, you can verify it belongs to you:"
msgstr ""
#: src/gettext_strings.rs:44
msgid "Verification Pending"
msgstr ""
#: src/gettext_strings.rs:45
msgid "<strong>Note:</strong> Some providers delay emails for up to 15 minutes to prevent spam. Please be patient."
msgstr ""
#: src/gettext_strings.rs:46
msgid "Send Verification Email"
msgstr ""
#: src/gettext_strings.rs:47
msgid "This key contains one identity that could not be parsed as an email address.<br /> This identity can't be published on <span class=\"brand\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-email-uids\" target=\"_blank\">Why?</a>)"
msgstr ""
#: src/gettext_strings.rs:48
msgid "This key contains {{ count_unparsed }} identities that could not be parsed as an email address.<br /> These identities can't be published on <span class=\"brand\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-email-uids\" target=\"_blank\">Why?</a>)"
msgstr ""
#: src/gettext_strings.rs:49
msgid "This key contains one revoked identity, which is not published. (<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
msgstr ""
#: src/gettext_strings.rs:50
msgid "This key contains {{ count_revoked }} revoked identities, which are not published. (<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
msgstr ""
#: src/gettext_strings.rs:51
msgid "Your keys have been successfully uploaded:"
msgstr ""
#: src/gettext_strings.rs:52
msgid "<strong>Note:</strong> To make keys searchable by email address, you must upload them individually."
msgstr ""
#: src/gettext_strings.rs:53
msgid "Verifying your email address…"
msgstr ""
#: src/gettext_strings.rs:54
msgid "If the process doesn't complete after a few seconds, please <input type=\"submit\" class=\"textbutton\" value=\"click here\" />."
msgstr ""
#: src/gettext_strings.rs:56
msgid "Manage your key on {{domain}}"
msgstr ""
#: src/gettext_strings.rs:58
msgid "Hi,"
msgstr ""
#: src/gettext_strings.rs:59
msgid "This is an automated message from <a href=\"{{base_uri}}\" style=\"text-decoration:none; color: #333\">{{domain}}</a>."
msgstr ""
#: src/gettext_strings.rs:60
msgid "If you didn't request this message, please ignore it."
msgstr ""
#: src/gettext_strings.rs:61
msgid "OpenPGP key: <tt>{{primary_fp}}</tt>"
msgstr ""
#: src/gettext_strings.rs:62
msgid "To manage and delete listed addresses on this key, please follow the link below:"
msgstr ""
#: src/gettext_strings.rs:63
msgid "You can find more info at <a href=\"{{base_uri}}/about\">{{domain}}/about</a>."
msgstr ""
#: src/gettext_strings.rs:64
msgid "distributing OpenPGP keys since 2019"
msgstr ""
#: src/gettext_strings.rs:67
msgid "This is an automated message from {{domain}}."
msgstr ""
#: src/gettext_strings.rs:69
msgid "OpenPGP key: {{primary_fp}}"
msgstr ""
#: src/gettext_strings.rs:71
msgid "You can find more info at {{base_uri}}/about"
msgstr ""
#: src/gettext_strings.rs:74
msgid "Verify {{userid}} for your key on {{domain}}"
msgstr ""
#: src/gettext_strings.rs:80
msgid "To let others find this key from your email address \"<a rel=\"nofollow\" href=\"#\" style=\"text-decoration:none; color: #333\">{{userid}}</a>\", please click the link below:"
msgstr ""
#: src/gettext_strings.rs:88
msgid "To let others find this key from your email address \"{{userid}}\",\nplease follow the link below:"
msgstr ""
#: src/web/manage.rs:103
msgid "This link is invalid or expired"
msgstr ""
#: src/web/manage.rs:129
msgid "Malformed address: {address}"
msgstr ""
#: src/web/manage.rs:136
msgid "No key for address: {address}"
msgstr ""
#: src/web/manage.rs:152
msgid "A request has already been sent for this address recently."
msgstr ""
#: src/web/vks.rs:112
msgid "Parsing of key data failed."
msgstr ""
#: src/web/vks.rs:121
msgid "Whoops, please don't upload secret keys!"
msgstr ""
#: src/web/vks.rs:134
msgid "No key uploaded."
msgstr ""
#: src/web/vks.rs:178
msgid "Error processing uploaded key."
msgstr ""
#: src/web/vks.rs:248
msgid "Upload session expired. Please try again."
msgstr ""
#: src/web/vks.rs:285
msgid "Invalid verification link."
msgstr ""

460
po/hagrid/pl.po Normal file
View File

@@ -0,0 +1,460 @@
# Translators:
# Wiktor Kwapisiewicz, 2019
#
msgid ""
msgstr ""
"Project-Id-Version: hagrid\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2019-09-27 18:05+0000\n"
"Last-Translator: Wiktor Kwapisiewicz, 2019\n"
"Language-Team: Polish (https://www.transifex.com/otf/teams/102430/pl/)\n"
"Language: pl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=4; plural=(n==1 ? 0 : (n%10>=2 && n%10<=4) && (n"
"%100<12 || n%100>14) ? 1 : n!=1 && (n%10>=0 && n%10<=1) || (n%10>=5 && n"
"%10<=9) || (n%100>=12 && n%100<=14) ? 2 : 3);\n"
#: src/mail.rs:107
msgctxt "Subject for verification email"
msgid "Verify {userid} for your key on {domain}"
msgstr "Zweryfikuj {userid} dla twojego klucza na {domain}"
#: src/mail.rs:140
msgctxt "Subject for manage email"
msgid "Manage your key on {domain}"
msgstr "Zarządzaj swoim kluczem na {domain}"
#: src/gettext_strings.rs:4
msgid "Error"
msgstr "Błąd"
#: src/gettext_strings.rs:5
msgid "Looks like something went wrong :("
msgstr "Wygląda na to, że coś poszło nie tak :("
#: src/gettext_strings.rs:6
msgid "Error message: {{ internal_error }}"
msgstr "Komunikat błędu: {{ internal_error }}"
#: src/gettext_strings.rs:7
msgid "There was an error with your request:"
msgstr "Podczas przetwarzania twojego zapytania wystąpił błąd:"
#: src/gettext_strings.rs:8
msgid "We found an entry for <span class=\"email\">{{ query }}</span>:"
msgstr "Znaleźliśmy wpis dla <span class=\"email\">{{ query }}</span>:"
#: src/gettext_strings.rs:9
msgid ""
"<strong>Hint:</strong> It's more convenient to use <span class=\"brand"
"\">keys.openpgp.org</span> from your OpenPGP software.<br /> Take a look at "
"our <a href=\"/about/usage\">usage guide</a> for details."
msgstr ""
"<strong>Podpowiedź:</strong> Wygodniej używać <span class=\"brand\">keys."
"openpgp.org</span> z twojego oprogramowania OpenPGP.<br /> Sprawdź nasz <a "
"href=\"/about/usage\">poradnik</a> aby dowiedzieć się więcej."
#: src/gettext_strings.rs:10
msgid "debug info"
msgstr "informacje diagnostyczne"
#: src/gettext_strings.rs:11
msgid "Search by Email Address / Key ID / Fingerprint"
msgstr "Wyszukaj po adresie e-mail / ID klucza / odcisku"
#: src/gettext_strings.rs:12
msgid "Search"
msgstr "Wyszukaj"
#: src/gettext_strings.rs:13
msgid ""
"You can also <a href=\"/upload\">upload</a> or <a href=\"/manage\">manage</"
"a> your key."
msgstr ""
"Możesz także <a href=\"/upload\">wgrać</a> lub <a href=\"/manage"
"\">zarządzać</a> swoim kluczem."
#: src/gettext_strings.rs:14
msgid "Find out more <a href=\"/about\">about this service</a>."
msgstr "Dowiedz się więcej <a href=\"/about\">o tej usłudze</a>."
#: src/gettext_strings.rs:15
msgid "News:"
msgstr "Nowości:"
#: src/gettext_strings.rs:16
msgid ""
"<a href=\"/about/news#2019-09-12-three-months-later\">Three months after "
"launch ✨</a> (2019-09-12)"
msgstr ""
"<a href=\"/about/news#2019-09-12-three-months-later\">Trzy miesiące od "
"startu ✨</a> (2019-09-12)"
#: src/gettext_strings.rs:17
msgid "v{{ version }} built from"
msgstr "v{{ version }} zbudowana z "
#: src/gettext_strings.rs:18
msgid "Powered by <a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>"
msgstr "Używa <a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>"
#: src/gettext_strings.rs:19
msgid ""
"Background image retrieved from <a href=\"https://www.toptal.com/designers/"
"subtlepatterns/subtle-grey/\">Subtle Patterns</a> under CC BY-SA 3.0"
msgstr ""
"Tło pobrane z <a href=\"https://www.toptal.com/designers/subtlepatterns/"
"subtle-grey/\">Subtle Patterns</a> na licencji CC BY-SA 3.0"
#: src/gettext_strings.rs:20
msgid "Maintenance Mode"
msgstr "Tryb Serwisowania"
#: src/gettext_strings.rs:21
msgid "Manage your key"
msgstr "Zarządzaj kluczem"
#: src/gettext_strings.rs:22
msgid "Enter any verified email address for your key"
msgstr "Wpisz dowolny zweryfikowany adres swojego klucza"
#: src/gettext_strings.rs:23
msgid "Send link"
msgstr "Wyślij łącze"
#: src/gettext_strings.rs:24
msgid ""
"We will send you an email with a link you can use to remove any of your "
"email addresses from search."
msgstr ""
"Wyślemy ci e-mail z łączem którego możesz użyć do usunięcia dowolnego adresu "
"z wyszukiwania."
#: src/gettext_strings.rs:25
msgid ""
"Managing the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
msgstr ""
"Zarządzanie kluczem <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>"
#: src/gettext_strings.rs:26
msgid "Your key is published with the following identity information:"
msgstr ""
"Twój klucz został opublikowany z następującymi informacjami tożsamości:"
#: src/gettext_strings.rs:27
msgid "Delete"
msgstr "Usuń"
#: src/gettext_strings.rs:28
msgid ""
"Clicking \"delete\" on any address will remove it from this key. It will no "
"longer appear in a search.<br /> To add another address, <a href=\"/upload"
"\">upload</a> the key again."
msgstr ""
"Klikając \"usuń\" na dowolnym adresie usunie ten adres z klucza. Nie będzie "
"on wyświetlany podczas wyszukiwania.<br /> Aby dodać inny adres <a href=\"/"
"upload\">wyślij</a> klucz ponownie."
#: src/gettext_strings.rs:29
msgid ""
"Your key is published as only non-identity information. (<a href=\"/about\" "
"target=\"_blank\">What does this mean?</a>)"
msgstr ""
"Twój klucz jest opublikowany bez informacji o tożsamości. (<a href=\"/about"
"\" target=\"_blank\">Co to oznacza?</a>)"
#: src/gettext_strings.rs:30
msgid "To add an address, <a href=\"/upload\">upload</a> the key again."
msgstr "Aby dodać adres, <a href=\"/upload\">wyślij</a> klucz ponownie."
#: src/gettext_strings.rs:31
msgid ""
"We have sent an email with further instructions to <span class=\"email"
"\">{{ address }}</span>."
msgstr ""
"Wysłaliśmy e-mail z dalszymi instrukcjami pod <span class=\"email"
"\">{{ address }}</span>."
#: src/gettext_strings.rs:32
msgid "This address has already been verified."
msgstr "Adres został już zweryfikowany."
#: src/gettext_strings.rs:33
msgid ""
"Your key <span class=\"fingerprint\">{{ key_fpr }}</span> is now published "
"for the identity <a href=\"{{userid_link}}\" target=\"_blank\"><span class="
"\"email\">{{ userid }}</span></a>."
msgstr ""
"Twój klucz <span class=\"fingerprint\">{{ key_fpr }}</span> jest "
"opublikowany dla tożsamości <a href=\"{{userid_link}}\" target=\"_blank"
"\"><span class=\"email\">{{ userid }}</span></a>."
#: src/gettext_strings.rs:34
msgid "Upload your key"
msgstr "Wyślij swój klucz"
#: src/gettext_strings.rs:35
msgid "Upload"
msgstr "Wyślij"
#: src/gettext_strings.rs:36
msgid ""
"Need more info? Check our <a target=\"_blank\" href=\"/about\">intro</a> and "
"<a target=\"_blank\" href=\"/about/usage\">usage guide</a>."
msgstr ""
"Potrzebujesz więcej informacji? Sprawdź nasze <a target=\"_blank\" href=\"/"
"about\">wprowadzenie</a> oraz <a target=\"_blank\" href=\"/about/usage"
"\">instrukcję użytkowania</a>."
#: src/gettext_strings.rs:37
msgid ""
"You uploaded the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
msgstr ""
"Wysłałeś klucz <span class=\"fingerprint\"><a href=\"{{ key_link }}\" target="
"\"_blank\">{{ key_fpr }}</a></span>."
#: src/gettext_strings.rs:38
msgid "This key is revoked."
msgstr "Ten klucz jest unieważniony."
#: src/gettext_strings.rs:39
msgid ""
"It is published without identity information and can't be made available for "
"search by email address (<a href=\"/about\" target=\"_blank\">what does this "
"mean?</a>)."
msgstr ""
"Jest opublikowany bez informacji o tożsamości i nie można go znaleźć za "
"pomocą adresu e-mail (<a href=\"/about\" target=\"_blank\">co to znaczy?</"
"a>)."
#: src/gettext_strings.rs:40
msgid ""
"This key is now published with the following identity information (<a href="
"\"/about\" target=\"_blank\">what does this mean?</a>):"
msgstr ""
"Ten klucz jest teraz opublikowany z następującymi informacjami tożsamości "
"(<a href=\"/about\" target=\"_blank\">co to znaczy?</a>);"
#: src/gettext_strings.rs:41
msgid "Published"
msgstr "Opublikowany"
#: src/gettext_strings.rs:42
msgid ""
"This key is now published with only non-identity information. (<a href=\"/"
"about\" target=\"_blank\">What does this mean?</a>)"
msgstr ""
"Ten klucz jest opublikowany bez informacji o tożsamości (<a href=\"/about\" "
"target=\"_blank\">Co to oznacza?</a>)"
#: src/gettext_strings.rs:43
msgid ""
"To make the key available for search by email address, you can verify it "
"belongs to you:"
msgstr ""
"Aby można było wyszukać ten klucz używając adresu e-mail, zweryfikuj, że "
"należy do ciebie:"
#: src/gettext_strings.rs:44
msgid "Verification Pending"
msgstr "Czekam na weryfikację"
#: src/gettext_strings.rs:45
msgid ""
"<strong>Note:</strong> Some providers delay emails for up to 15 minutes to "
"prevent spam. Please be patient."
msgstr ""
"<strong>Informacja:</strong> Niektórzy dostawcy poczty opóźniają e-maile do "
"15 minut aby zapobiegać spamowi. Proszę być cierpliwym."
#: src/gettext_strings.rs:46
msgid "Send Verification Email"
msgstr "Wyślij E-mail Weryfikacyjny"
#: src/gettext_strings.rs:47
msgid ""
"This key contains one identity that could not be parsed as an email address."
"<br /> This identity can't be published on <span class=\"brand\">keys."
"openpgp.org</span>. (<a href=\"/about/faq#non-email-uids\" target=\"_blank"
"\">Why?</a>)"
msgstr ""
"Ten klucz posiada jedną tożsamość z której nie można wyłuskać adresu e-mail."
"<br /> Ta tożsamość nie może zostać opublikowana na <span class=\"brand"
"\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-email-uids\" target="
"\"_blank\">Dlaczego?</a>)"
#: src/gettext_strings.rs:48
msgid ""
"This key contains {{ count_unparsed }} identities that could not be parsed "
"as an email address.<br /> These identities can't be published on <span "
"class=\"brand\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-email-"
"uids\" target=\"_blank\">Why?</a>)"
msgstr ""
"Ten klucz zawiera {{ count_unparsed }} tożsamości z których nie da się "
"wyłuskać adresów e-mail.<br /> Te tożsamości nie mogą zostać opublikowane na "
"<span class=\"brand\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-"
"email-uids\" target=\"_blank\">Dlaczego?</a>)"
#: src/gettext_strings.rs:49
msgid ""
"This key contains one revoked identity, which is not published. (<a href=\"/"
"about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
msgstr ""
"Ten klucz zwiera jedną unieważnioną tożsamość które nie została "
"opublikowana. (<a href=\"/about/faq#revoked-uids\" target=\"_blank"
"\">Dlaczego?</a>)"
#: src/gettext_strings.rs:50
msgid ""
"This key contains {{ count_revoked }} revoked identities, which are not "
"published. (<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
msgstr ""
"Ten klucz zwiera {{ count_revoked }} unieważnionych tożsamości które nie "
"zostały opublikowane. (<a href=\"/about/faq#revoked-uids\" target=\"_blank"
"\">Dlaczego?</a>)"
#: src/gettext_strings.rs:51
msgid "Your keys have been successfully uploaded:"
msgstr "Twoje klucze zostały pomyślnie wysłane:"
#: src/gettext_strings.rs:52
msgid ""
"<strong>Note:</strong> To make keys searchable by email address, you must "
"upload them individually."
msgstr ""
"<strong>Informacja:</strong> Aby można było wyszukać kluczy za pomocą adresu "
"e-mail, wyślij je pojedynczo."
#: src/gettext_strings.rs:53
msgid "Verifying your email address…"
msgstr "Weryfikowanie twojego adresu e-mail..."
#: src/gettext_strings.rs:54
msgid ""
"If the process doesn't complete after a few seconds, please <input type="
"\"submit\" class=\"textbutton\" value=\"click here\" />."
msgstr ""
"Jeśli proces nie zakończy się po kilku sekundach proszę <input type=\"submit"
"\" class=\"textbutton\" value=\"kliknij tutaj\"/>."
#: src/gettext_strings.rs:56
msgid "Manage your key on {{domain}}"
msgstr "Zarządzaj swoim kluczem na {{domain}}"
#: src/gettext_strings.rs:58
msgid "Hi,"
msgstr "Cześć,"
#: src/gettext_strings.rs:59
msgid ""
"This is an automated message from <a href=\"{{base_uri}}\" style=\"text-"
"decoration:none; color: #333\">{{domain}}</a>."
msgstr ""
"To jest automatyczna wiadomość od <a href=\"{{base_uri}}\" style=\"text-"
"decoration:none; color: #333\">{{domain}}</a>."
#: src/gettext_strings.rs:60
msgid "If you didn't request this message, please ignore it."
msgstr "Jeśli nie prosiłeś o tą wiadomość, zignorują ją."
#: src/gettext_strings.rs:61
msgid "OpenPGP key: <tt>{{primary_fp}}</tt>"
msgstr "Klucz OpenPGP: <tt>{{primary_fp}}</tt>"
#: src/gettext_strings.rs:62
msgid ""
"To manage and delete listed addresses on this key, please follow the link "
"below:"
msgstr "Aby zarządzać adresami na kluczu lub je usunąć, użyj tego łącza:"
#: src/gettext_strings.rs:63
msgid ""
"You can find more info at <a href=\"{{base_uri}}/about\">{{domain}}/about</"
"a>."
msgstr ""
"Więcej informacji znajdziesz na <a href=\"{{base_uri}}/about\">{{domain}}/"
"about</a>."
#: src/gettext_strings.rs:64
msgid "distributing OpenPGP keys since 2019"
msgstr "dostarczanie kluczy OpenPGP od 2019"
#: src/gettext_strings.rs:67
msgid "This is an automated message from {{domain}}."
msgstr "To automatyczna wiadomość z {{domain}}."
#: src/gettext_strings.rs:69
msgid "OpenPGP key: {{primary_fp}}"
msgstr "Klucz OpenPGP: {{primary_fp}}"
#: src/gettext_strings.rs:71
msgid "You can find more info at {{base_uri}}/about"
msgstr "Znajdziesz więcej informacji na {{base_uri}}/about"
#: src/gettext_strings.rs:74
msgid "Verify {{userid}} for your key on {{domain}}"
msgstr "Zweryfikuj {{userid}} dla klucza na {{domain}}"
#: src/gettext_strings.rs:80
msgid ""
"To let others find this key from your email address \"<a rel=\"nofollow\" "
"href=\"#\" style=\"text-decoration:none; color: #333\">{{userid}}</a>\", "
"please click the link below:"
msgstr ""
"Aby inni mogli wyszukać twój klucz przez adres e-mail \"<a rel=\"nofollow\" "
"href=\"#\" style=\"text-decoration:none; color: #333\">{{userid}}</a>\", "
"proszę kliknąć w łącze poniżej:"
#: src/gettext_strings.rs:88
msgid ""
"To let others find this key from your email address \"{{userid}}\",\n"
"please follow the link below:"
msgstr ""
"Aby inni mogli wyszukać twój klucz przez adres e-mail \"{{userid}}\", proszę "
"kliknąć w łącze poniżej:"
#: src/web/manage.rs:103
msgid "This link is invalid or expired"
msgstr "Łącze jest nieważne lub wygasło"
#: src/web/manage.rs:129
msgid "Malformed address: {address}"
msgstr "Nieprawidłowy adres: {address}"
#: src/web/manage.rs:136
msgid "No key for address: {address}"
msgstr "Brak klucza dla adresu: {address}"
#: src/web/manage.rs:152
msgid "A request has already been sent for this address recently."
msgstr "Zapytanie o ten adres zostało niedawno wysłane."
#: src/web/vks.rs:112
msgid "Parsing of key data failed."
msgstr "Przetwarzanie danych klucza się nie powiodło."
#: src/web/vks.rs:121
msgid "Whoops, please don't upload secret keys!"
msgstr "Ups, nie wysyłaj kluczy prywatnych!"
#: src/web/vks.rs:134
msgid "No key uploaded."
msgstr "Klucz nie został wysłany."
#: src/web/vks.rs:178
msgid "Error processing uploaded key."
msgstr "Błąd przetwarzania wysłanego klucza."
#: src/web/vks.rs:248
msgid "Upload session expired. Please try again."
msgstr "Sesja wysyłania wygasła. Spróbuj ponownie."
#: src/web/vks.rs:285
msgid "Invalid verification link."
msgstr "Nieprawidłowy link weryfikacyjny."

459
po/hagrid/tr.po Normal file
View File

@@ -0,0 +1,459 @@
# Translators:
# T. E. Kalayci <tekrei@gmail.com>, 2019
#
msgid ""
msgstr ""
"Project-Id-Version: hagrid\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2019-09-27 18:05+0000\n"
"Last-Translator: T. E. Kalayci <tekrei@gmail.com>, 2019\n"
"Language-Team: Turkish (https://www.transifex.com/otf/teams/102430/tr/)\n"
"Language: tr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: src/mail.rs:107
msgctxt "Subject for verification email"
msgid "Verify {userid} for your key on {domain}"
msgstr "{domain} üzerindeki anahtarınız için {userid} doğrulaması"
#: src/mail.rs:140
msgctxt "Subject for manage email"
msgid "Manage your key on {domain}"
msgstr "{domain} sunucusundaki anahtarınızı yönetin"
#: src/gettext_strings.rs:4
msgid "Error"
msgstr "Hata"
#: src/gettext_strings.rs:5
msgid "Looks like something went wrong :("
msgstr "Bir şeyler ters gitti gibi gözüküyor :("
#: src/gettext_strings.rs:6
msgid "Error message: {{ internal_error }}"
msgstr "Hata mesajı: {{ internal_error }}"
#: src/gettext_strings.rs:7
msgid "There was an error with your request:"
msgstr "İsteğinizle ilgili bir hata oluştu:"
#: src/gettext_strings.rs:8
msgid "We found an entry for <span class=\"email\">{{ query }}</span>:"
msgstr "<span class=\"email\">{{ query }}</span> için bir girdi bulundu:"
#: src/gettext_strings.rs:9
msgid ""
"<strong>Hint:</strong> It's more convenient to use <span class=\"brand"
"\">keys.openpgp.org</span> from your OpenPGP software.<br /> Take a look at "
"our <a href=\"/about/usage\">usage guide</a> for details."
msgstr ""
"<strong>İpucu:</strong> <span class=\"brand\">keys.openpgp.org</span> "
"hizmetini OpenPGP yazılımından kullanmak daha elverişlidir.<br />Ayrıntılar "
"için <a href=\"/about/usage\">kullanma kılavuzumuza</a> bakabilirsiniz."
#: src/gettext_strings.rs:10
msgid "debug info"
msgstr "hata ayıklama bilgileri"
#: src/gettext_strings.rs:11
msgid "Search by Email Address / Key ID / Fingerprint"
msgstr "E-posta adresi / Anahtar Kimliği / Parmak İzi ile arama"
#: src/gettext_strings.rs:12
msgid "Search"
msgstr "Ara"
#: src/gettext_strings.rs:13
msgid ""
"You can also <a href=\"/upload\">upload</a> or <a href=\"/manage\">manage</"
"a> your key."
msgstr ""
"Ayrıca anahtarınızı <a href=\"/upload\">yükleyebilir</a> veya <a href=\"/"
"manage\">yönetebilirsiniz</a>."
#: src/gettext_strings.rs:14
msgid "Find out more <a href=\"/about\">about this service</a>."
msgstr "<a href=\"/about\">Bu hizmet hakkında</a> daha fazlasını öğrenin."
#: src/gettext_strings.rs:15
msgid "News:"
msgstr "Haberler:"
#: src/gettext_strings.rs:16
msgid ""
"<a href=\"/about/news#2019-09-12-three-months-later\">Three months after "
"launch ✨</a> (2019-09-12)"
msgstr ""
"<a href=\"/about/news#2019-09-12-three-months-later\">Başlangıçtan itibaren "
"üç ay✨</a> (12 Eylül 2019)"
#: src/gettext_strings.rs:17
msgid "v{{ version }} built from"
msgstr "v{{ version }} dayandığı sürüm"
#: src/gettext_strings.rs:18
msgid "Powered by <a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>"
msgstr "<a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>desteklidir"
#: src/gettext_strings.rs:19
msgid ""
"Background image retrieved from <a href=\"https://www.toptal.com/designers/"
"subtlepatterns/subtle-grey/\">Subtle Patterns</a> under CC BY-SA 3.0"
msgstr ""
"Arkaplan resmi <a href=\"https://www.toptal.com/designers/subtlepatterns/"
"subtle-grey/\">Subtle Patterns</a>tarafından CC BY-SA 3.0 lisansıyla "
"sağlanmıştır."
#: src/gettext_strings.rs:20
msgid "Maintenance Mode"
msgstr "Bakım Modu"
#: src/gettext_strings.rs:21
msgid "Manage your key"
msgstr "Anahtarınızı yönetin"
#: src/gettext_strings.rs:22
msgid "Enter any verified email address for your key"
msgstr "Anahtarınız için doğrulanmış bir e-posta adresi girin"
#: src/gettext_strings.rs:23
msgid "Send link"
msgstr "Bağlantıyı gönder"
#: src/gettext_strings.rs:24
msgid ""
"We will send you an email with a link you can use to remove any of your "
"email addresses from search."
msgstr ""
"E-posta adreslerinizi aramadan kaldırabilmek için kullanabileceğiniz bir "
"bağlantıyı size e-posta olarak göndereceğiz."
#: src/gettext_strings.rs:25
msgid ""
"Managing the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
msgstr ""
"<span class=\"fingerprint\"><a href=\"{{ key_link }}\" target=\"_blank"
"\">{{ key_fpr }}</a></span> anahtarı yönetiliyor."
#: src/gettext_strings.rs:26
msgid "Your key is published with the following identity information:"
msgstr "Anahtarınız aşağıdaki kimlik bilgileriyle yayınlanıyor:"
#: src/gettext_strings.rs:27
msgid "Delete"
msgstr "Sil"
#: src/gettext_strings.rs:28
msgid ""
"Clicking \"delete\" on any address will remove it from this key. It will no "
"longer appear in a search.<br /> To add another address, <a href=\"/upload"
"\">upload</a> the key again."
msgstr ""
"Herhangi bir adreste \"sil\"e tıklamanız durumunda, o adresi anahtardan "
"kaldıracaktır. Aramada artık gözükmeyecektir. <br />Başka bir adres eklemek "
"için anahtarı tekrar <a href=\"/upload\">yükleyin</a> ."
#: src/gettext_strings.rs:29
msgid ""
"Your key is published as only non-identity information. (<a href=\"/about\" "
"target=\"_blank\">What does this mean?</a>)"
msgstr ""
"Anahtarınız kimlik bilgisi içermeden yayınlanıyor. (<a href=\"/about\" "
"target=\"_blank\">Bunun anlamı nedir</a>)"
#: src/gettext_strings.rs:30
msgid "To add an address, <a href=\"/upload\">upload</a> the key again."
msgstr ""
"Bir adres eklemek için anahtarı tekrar <a href=\"/upload\">yükleyin</a>."
#: src/gettext_strings.rs:31
msgid ""
"We have sent an email with further instructions to <span class=\"email"
"\">{{ address }}</span>."
msgstr ""
"Ek yönergeleri içeren bir e-postayı <span class=\"email\">{{ address }}</"
"span> adresine gönderdik."
#: src/gettext_strings.rs:32
msgid "This address has already been verified."
msgstr "Bu adres zaten doğrulanmış."
#: src/gettext_strings.rs:33
msgid ""
"Your key <span class=\"fingerprint\">{{ key_fpr }}</span> is now published "
"for the identity <a href=\"{{userid_link}}\" target=\"_blank\"><span class="
"\"email\">{{ userid }}</span></a>."
msgstr ""
"<span class=\"fingerprint\">{{ key_fpr }}</span> anahtarınız şimdi <a href="
"\"{{userid_link}}\" target=\"_blank\"><span class=\"email\">{{ userid }}</"
"span></a> kimliği için yayınlandı."
#: src/gettext_strings.rs:34
msgid "Upload your key"
msgstr "Anahtarınızı yükleyin"
#: src/gettext_strings.rs:35
msgid "Upload"
msgstr "Yükle"
#: src/gettext_strings.rs:36
msgid ""
"Need more info? Check our <a target=\"_blank\" href=\"/about\">intro</a> and "
"<a target=\"_blank\" href=\"/about/usage\">usage guide</a>."
msgstr ""
"Daha fazla bilgi için<a target=\"_blank\" href=\"/about\">tanıtım</a> veya "
"<a target=\"_blank\" href=\"/about/usage\">kullanma kılavuzuna</a> "
"bakabilirsiniz."
#: src/gettext_strings.rs:37
msgid ""
"You uploaded the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
msgstr ""
"<span class=\"fingerprint\"><a href=\"{{ key_link }}\" target=\"_blank"
"\">{{ key_fpr }}</a></span> anahtarını yüklediniz."
#: src/gettext_strings.rs:38
msgid "This key is revoked."
msgstr "Bu anahtar feshedildi."
#: src/gettext_strings.rs:39
msgid ""
"It is published without identity information and can't be made available for "
"search by email address (<a href=\"/about\" target=\"_blank\">what does this "
"mean?</a>)."
msgstr ""
"Kimlik bilgisi olmayan yayınlandı ve e-posta aramasıyla bulunamaz (<a href="
"\"/about\" target=\"_blank\">bunun anlamı nedir?</a>)"
#: src/gettext_strings.rs:40
msgid ""
"This key is now published with the following identity information (<a href="
"\"/about\" target=\"_blank\">what does this mean?</a>):"
msgstr ""
"Bu anahtar şimdi aşağıdaki kimlik bilgisiyle yayınlandı (<a href=\"/about\" "
"target=\"_blank\">bunun anlamı nedir?</a>)"
#: src/gettext_strings.rs:41
msgid "Published"
msgstr "Yayınlandı"
#: src/gettext_strings.rs:42
msgid ""
"This key is now published with only non-identity information. (<a href=\"/"
"about\" target=\"_blank\">What does this mean?</a>)"
msgstr ""
"Bu anahtar şimdi sadece kimlik olmayan bilgiyle yayınlandı (<a href=\"/about"
"\" target=\"_blank\">bunun anlamı nedir?</a>)"
#: src/gettext_strings.rs:43
msgid ""
"To make the key available for search by email address, you can verify it "
"belongs to you:"
msgstr ""
"Anahtarı e-posta adresiyle bulunabilir yapmak için, size ait olduğunu "
"doğrulamanız gerekiyor:"
#: src/gettext_strings.rs:44
msgid "Verification Pending"
msgstr "Doğrulama Bekleniyor"
#: src/gettext_strings.rs:45
msgid ""
"<strong>Note:</strong> Some providers delay emails for up to 15 minutes to "
"prevent spam. Please be patient."
msgstr ""
"<strong>Not:</strong> Bazı sağlayıcılar istenmeyen e-postaları önlemek "
"amacıyla e-postaları 15 dakikaya kadar geciktirebiliyor. Lütfen sabırla "
"bekleyin."
#: src/gettext_strings.rs:46
msgid "Send Verification Email"
msgstr "Doğrulama E-postasını Gönder"
#: src/gettext_strings.rs:47
msgid ""
"This key contains one identity that could not be parsed as an email address."
"<br /> This identity can't be published on <span class=\"brand\">keys."
"openpgp.org</span>. (<a href=\"/about/faq#non-email-uids\" target=\"_blank"
"\">Why?</a>)"
msgstr ""
"Bu anahtar e-posta adresi olarak çözümlenemeyen bir kimlik içeriyor. <br /"
">Bu kimlik bilgisi <span class=\"brand\">keys.openpgp.org</span>hizmetinde "
"yayınlanamaz. (<a href=\"/about/faq#non-email-uids\" target=\"_blank\">Neden?"
"</a>)"
#: src/gettext_strings.rs:48
msgid ""
"This key contains {{ count_unparsed }} identities that could not be parsed "
"as an email address.<br /> These identities can't be published on <span "
"class=\"brand\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-email-"
"uids\" target=\"_blank\">Why?</a>)"
msgstr ""
"Bu anahtar e-posta adresi olarak çözümlenemeyen {{ count_unparsed }} kimlik "
"bilgisi içeriyor. <br />Bu kimlik bilgileri <span class=\"brand\">keys."
"openpgp.org</span> hizmtinde yayınlanamaz. (<a href=\"/about/faq#non-email-"
"uids\" target=\"_blank\">Neden?</a>)"
#: src/gettext_strings.rs:49
msgid ""
"This key contains one revoked identity, which is not published. (<a href=\"/"
"about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
msgstr ""
"Bu anahtar feshedilmiş, yayınlanmayan bir kimlik içeriyor. (<a href=\"/about/"
"faq#revoked-uids\" target=\"_blank\">Neden?</a>)"
#: src/gettext_strings.rs:50
msgid ""
"This key contains {{ count_revoked }} revoked identities, which are not "
"published. (<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
msgstr ""
"Bu anahtar feshedilmiş, yayınlanmayan {{ count_revoked }} kimlik bilgisi "
"içeriyor. (<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Neden?</a>)"
#: src/gettext_strings.rs:51
msgid "Your keys have been successfully uploaded:"
msgstr "Anahtarlarınız başarılı bir şekilde yüklendi:"
#: src/gettext_strings.rs:52
msgid ""
"<strong>Note:</strong> To make keys searchable by email address, you must "
"upload them individually."
msgstr ""
"<strong>Not:</strong> Anahtarlarınızı e-posta adresiyle bulunabilir yapmak "
"için, her birini ayrı ayrı yüklemeniz gerekiyor."
#: src/gettext_strings.rs:53
msgid "Verifying your email address…"
msgstr "E-posta adresiniz doğrulanıyor..."
#: src/gettext_strings.rs:54
msgid ""
"If the process doesn't complete after a few seconds, please <input type="
"\"submit\" class=\"textbutton\" value=\"click here\" />."
msgstr ""
"Eğer işlem bir kaç saniye içerisinde tamamlanmazsa, lütfen <input type="
"\"submit\" class=\"textbutton\" value=\"click here\" />."
#: src/gettext_strings.rs:56
msgid "Manage your key on {{domain}}"
msgstr "{{ domain }} üzerindeki anahtarınızı yönetin"
#: src/gettext_strings.rs:58
msgid "Hi,"
msgstr "Merhaba,"
#: src/gettext_strings.rs:59
msgid ""
"This is an automated message from <a href=\"{{base_uri}}\" style=\"text-"
"decoration:none; color: #333\">{{domain}}</a>."
msgstr ""
"Bu <a href=\"{{base_uri}}\" style=\"text-decoration:none; color: "
"#333\">{{domain}}</a> tarafından gönderilen otomatik bir mesajdır. "
#: src/gettext_strings.rs:60
msgid "If you didn't request this message, please ignore it."
msgstr "Eğer bu mesajı siz talep etmediyseniz, lütfen görmezden gelin."
#: src/gettext_strings.rs:61
msgid "OpenPGP key: <tt>{{primary_fp}}</tt>"
msgstr "OpenPGP anahtarı: <tt>{{primary_fp}}</tt>"
#: src/gettext_strings.rs:62
msgid ""
"To manage and delete listed addresses on this key, please follow the link "
"below:"
msgstr ""
"Bu anahtarla listelenmiş adresleri yönetmek ve silmek için, lütfen aşağıdaki "
"bağlantıya tıklayın:"
#: src/gettext_strings.rs:63
msgid ""
"You can find more info at <a href=\"{{base_uri}}/about\">{{domain}}/about</"
"a>."
msgstr ""
"Daha fazla bilgiyi <a href=\"{{base_uri}}/about\">{{domain}}/about</a> "
"sayfasında bulabilirsiniz."
#: src/gettext_strings.rs:64
msgid "distributing OpenPGP keys since 2019"
msgstr "OpenPGP anahtarları 2019 yılından beri dağıtılıyor"
#: src/gettext_strings.rs:67
msgid "This is an automated message from {{domain}}."
msgstr "Bu {{domain}} tarafından gönderilen otomatik bir mesajdır. "
#: src/gettext_strings.rs:69
msgid "OpenPGP key: {{primary_fp}}"
msgstr "OpenPGP anahtarı: {{primary_fp}}"
#: src/gettext_strings.rs:71
msgid "You can find more info at {{base_uri}}/about"
msgstr "Daha fazla bilgiyi {{base_uri}}/about sayfasında bulabilirsiniz."
#: src/gettext_strings.rs:74
msgid "Verify {{userid}} for your key on {{domain}}"
msgstr "{{userid}} kimliğini {{domain}} üzerindeki anahtarınız için doğrulayın"
#: src/gettext_strings.rs:80
msgid ""
"To let others find this key from your email address \"<a rel=\"nofollow\" "
"href=\"#\" style=\"text-decoration:none; color: #333\">{{userid}}</a>\", "
"please click the link below:"
msgstr ""
"Başkalarının bu anahtarı, \"<a rel=\"nofollow\" href=\"#\" style=\"text-"
"decoration:none; color: #333\">{{userid}}</a>\" e-posta adresinizi "
"kullanarak bulmasını istiyorsanız lütfen aşağıdaki bağlantıya tıklayın:"
#: src/gettext_strings.rs:88
msgid ""
"To let others find this key from your email address \"{{userid}}\",\n"
"please follow the link below:"
msgstr ""
"Başkalarının bu anahtarı, \"{{userid}}\" e-posta adresinizi kullanarak "
"bulmasını istiyorsanız lütfen aşağıdaki bağlantıya gidin:"
#: src/web/manage.rs:103
msgid "This link is invalid or expired"
msgstr "Bu bağlantı geçersiz veya süresi geçmiş."
#: src/web/manage.rs:129
msgid "Malformed address: {address}"
msgstr "Hatalı adres: {address}"
#: src/web/manage.rs:136
msgid "No key for address: {address}"
msgstr "Anahtarsız adres: {address}"
#: src/web/manage.rs:152
msgid "A request has already been sent for this address recently."
msgstr "Bu adres için bir istek kısa bir süre önce zaten gönderilmişti."
#: src/web/vks.rs:112
msgid "Parsing of key data failed."
msgstr "Anahtar verisi çözümlemesi başarısız oldu."
#: src/web/vks.rs:121
msgid "Whoops, please don't upload secret keys!"
msgstr "Eyvah! Lütfen gizli anahtarınızı yüklemeyin!"
#: src/web/vks.rs:134
msgid "No key uploaded."
msgstr "Anahtar yüklenmedi."
#: src/web/vks.rs:178
msgid "Error processing uploaded key."
msgstr "Yüklenen anahtar işlenirken bir hata oluştu."
#: src/web/vks.rs:248
msgid "Upload session expired. Please try again."
msgstr "Yükleme oturumunun süresi doldu. Lütfen tekrar deneyin."
#: src/web/vks.rs:285
msgid "Invalid verification link."
msgstr "Geçersiz doğrulama bağlantısı."

447
po/hagrid/zh_Hans.po Normal file
View File

@@ -0,0 +1,447 @@
# Translators:
# 德夫 乔 <dszw2000@gmail.com>, 2019
#
msgid ""
msgstr ""
"Project-Id-Version: hagrid\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2018-06-15 16:33-0700\n"
"PO-Revision-Date: 2019-09-27 18:05+0000\n"
"Last-Translator: 德夫 乔 <dszw2000@gmail.com>, 2019\n"
"Language-Team: Chinese Simplified (https://www.transifex.com/otf/"
"teams/102430/zh-Hans/)\n"
"Language: zh-Hans\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
#: src/mail.rs:107
msgctxt "Subject for verification email"
msgid "Verify {userid} for your key on {domain}"
msgstr "在 {domain} 上验证你的密钥 {userid}"
#: src/mail.rs:140
msgctxt "Subject for manage email"
msgid "Manage your key on {domain}"
msgstr "在 {domain} 上管理你的密钥"
#: src/gettext_strings.rs:4
msgid "Error"
msgstr "错误"
#: src/gettext_strings.rs:5
msgid "Looks like something went wrong :("
msgstr "看上去有些东西出错了 :("
#: src/gettext_strings.rs:6
msgid "Error message: {{ internal_error }}"
msgstr "错误信息:{{ internal_error }}"
#: src/gettext_strings.rs:7
msgid "There was an error with your request:"
msgstr "你的请求有一个错误:"
#: src/gettext_strings.rs:8
msgid "We found an entry for <span class=\"email\">{{ query }}</span>:"
msgstr "我们找到了询问的地址<span class=\"email\">{{ query }}</span>"
#: src/gettext_strings.rs:9
msgid ""
"<strong>Hint:</strong> It's more convenient to use <span class=\"brand"
"\">keys.openpgp.org</span> from your OpenPGP software.<br /> Take a look at "
"our <a href=\"/about/usage\">usage guide</a> for details."
msgstr ""
"<strong>提示:</strong>在你的OpenPGP软件上使用<span class=\"brand\">keys."
"openpgp.org</span>会更方便。<br />查阅我们的<a href=\"/about/usage\">用户指南"
"</a>获取细节。"
#: src/gettext_strings.rs:10
msgid "debug info"
msgstr "调试信息"
#: src/gettext_strings.rs:11
msgid "Search by Email Address / Key ID / Fingerprint"
msgstr "通过电子邮件地址 / 密钥ID / 指纹搜索"
#: src/gettext_strings.rs:12
msgid "Search"
msgstr "搜索"
#: src/gettext_strings.rs:13
msgid ""
"You can also <a href=\"/upload\">upload</a> or <a href=\"/manage\">manage</"
"a> your key."
msgstr ""
"你也可以<a href=\"/upload\">上传</a>或<a href=\"/manage\">管理</a>你的密钥。"
#: src/gettext_strings.rs:14
msgid "Find out more <a href=\"/about\">about this service</a>."
msgstr "找到<a href=\"/about\">关于这项服务</a>的更多信息。"
#: src/gettext_strings.rs:15
msgid "News:"
msgstr "新闻:"
#: src/gettext_strings.rs:16
msgid ""
"<a href=\"/about/news#2019-09-12-three-months-later\">Three months after "
"launch ✨</a> (2019-09-12)"
msgstr ""
"<a href=\"/about/news#2019-09-12-three-months-later\">已成功运行三个月✨</"
"a>(2019-09-12)"
#: src/gettext_strings.rs:17
msgid "v{{ version }} built from"
msgstr "v{{ version }} built from"
#: src/gettext_strings.rs:18
msgid "Powered by <a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>"
msgstr "由<a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>支持"
#: src/gettext_strings.rs:19
msgid ""
"Background image retrieved from <a href=\"https://www.toptal.com/designers/"
"subtlepatterns/subtle-grey/\">Subtle Patterns</a> under CC BY-SA 3.0"
msgstr ""
"背景图从<a href=\"https://www.toptal.com/designers/subtlepatterns/subtle-"
"grey/\">Subtle Patterns</a>依据CC BY-SA 3.0协议获得"
#: src/gettext_strings.rs:20
msgid "Maintenance Mode"
msgstr "修复模式"
#: src/gettext_strings.rs:21
msgid "Manage your key"
msgstr "管理你的密钥"
#: src/gettext_strings.rs:22
msgid "Enter any verified email address for your key"
msgstr "输入任意关联到你的密钥的电子邮件地址"
#: src/gettext_strings.rs:23
msgid "Send link"
msgstr "发送链接"
#: src/gettext_strings.rs:24
msgid ""
"We will send you an email with a link you can use to remove any of your "
"email addresses from search."
msgstr ""
"我们将把链接用电子邮件的形式发给你,那条链接可以让你在搜索结果中移除你的任意"
"邮箱地址"
#: src/gettext_strings.rs:25
msgid ""
"Managing the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
msgstr ""
"管理密钥<span class=\"fingerprint\"><a href=\"{{ key_link }}\" target="
"\"_blank\">{{ key_fpr }}</a></span>"
#: src/gettext_strings.rs:26
msgid "Your key is published with the following identity information:"
msgstr "你的密钥已经用以下身份发布:"
#: src/gettext_strings.rs:27
msgid "Delete"
msgstr "删除"
#: src/gettext_strings.rs:28
msgid ""
"Clicking \"delete\" on any address will remove it from this key. It will no "
"longer appear in a search.<br /> To add another address, <a href=\"/upload"
"\">upload</a> the key again."
msgstr ""
"点击“删除”将会在密钥中删除该地址的关联。之后该地址将不会在搜索结果中出现。"
"<br />如果需要添加另外的地址,请<a href=\"/upload\">重新上传此密钥</a>"
#: src/gettext_strings.rs:29
msgid ""
"Your key is published as only non-identity information. (<a href=\"/about\" "
"target=\"_blank\">What does this mean?</a>)"
msgstr ""
"你的密钥以无身份的形式发布。(<a href=\"/about\" target=\"_blank\">这意味着什"
"么?</a>"
#: src/gettext_strings.rs:30
msgid "To add an address, <a href=\"/upload\">upload</a> the key again."
msgstr "如果要添加地址,<a href=\"/upload\">重新上传此密钥</a>"
#: src/gettext_strings.rs:31
msgid ""
"We have sent an email with further instructions to <span class=\"email"
"\">{{ address }}</span>."
msgstr ""
"我们已经向<span class=\"email\">{{ address }}</span>发送了包含后续说明的电子"
"邮件。"
#: src/gettext_strings.rs:32
msgid "This address has already been verified."
msgstr "该地址已经被验证过了。"
#: src/gettext_strings.rs:33
msgid ""
"Your key <span class=\"fingerprint\">{{ key_fpr }}</span> is now published "
"for the identity <a href=\"{{userid_link}}\" target=\"_blank\"><span class="
"\"email\">{{ userid }}</span></a>."
msgstr ""
"你的密钥<span class=\"fingerprint\">{{ key_fpr }}</span>已经以<a href="
"\"{{userid_link}}\" target=\"_blank\"><span class=\"email\">{{ userid }}</"
"span></a>的身份发布"
#: src/gettext_strings.rs:34
msgid "Upload your key"
msgstr "上传你的密钥"
#: src/gettext_strings.rs:35
msgid "Upload"
msgstr "上传"
#: src/gettext_strings.rs:36
msgid ""
"Need more info? Check our <a target=\"_blank\" href=\"/about\">intro</a> and "
"<a target=\"_blank\" href=\"/about/usage\">usage guide</a>."
msgstr ""
"想了解更多信息?查阅我们的<a target=\"_blank\" href=\"/about\">介绍</a>和<a "
"target=\"_blank\" href=\"/about/usage\">用户指南</a>。"
#: src/gettext_strings.rs:37
msgid ""
"You uploaded the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" "
"target=\"_blank\">{{ key_fpr }}</a></span>."
msgstr ""
"你上传了密钥<span class=\"fingerprint\"><a href=\"{{ key_link }}\" target="
"\"_blank\">{{ key_fpr }}</a></span>。"
#: src/gettext_strings.rs:38
msgid "This key is revoked."
msgstr "该密钥已经被吊销。"
#: src/gettext_strings.rs:39
msgid ""
"It is published without identity information and can't be made available for "
"search by email address (<a href=\"/about\" target=\"_blank\">what does this "
"mean?</a>)."
msgstr ""
"该密钥以无身份信息的形式发布,不能使用电子邮件地址搜索到(<a href=\"/about\" "
"target=\"_blank\">这意味着什么?</a>"
#: src/gettext_strings.rs:40
msgid ""
"This key is now published with the following identity information (<a href="
"\"/about\" target=\"_blank\">what does this mean?</a>):"
msgstr ""
"该密钥已经用以下身份信息发布(<a href=\"/about\" target=\"_blank\">这意味着什"
"么?</a>"
#: src/gettext_strings.rs:41
msgid "Published"
msgstr "已发布"
#: src/gettext_strings.rs:42
msgid ""
"This key is now published with only non-identity information. (<a href=\"/"
"about\" target=\"_blank\">What does this mean?</a>)"
msgstr ""
"你的密钥现在以无身份的形式发布。(<a href=\"/about\" target=\"_blank\">这意味"
"着什么?</a>"
#: src/gettext_strings.rs:43
msgid ""
"To make the key available for search by email address, you can verify it "
"belongs to you:"
msgstr "要让密钥能通过电子邮件地址搜索到,你可以验证该地址属于你:"
#: src/gettext_strings.rs:44
msgid "Verification Pending"
msgstr "等待验证"
#: src/gettext_strings.rs:45
msgid ""
"<strong>Note:</strong> Some providers delay emails for up to 15 minutes to "
"prevent spam. Please be patient."
msgstr ""
"<strong>提醒:</strong>部分服务商会将邮件拖延最多15分钟来阻止垃圾邮件。请保持"
"耐心。"
#: src/gettext_strings.rs:46
msgid "Send Verification Email"
msgstr "发送验证邮件"
#: src/gettext_strings.rs:47
msgid ""
"This key contains one identity that could not be parsed as an email address."
"<br /> This identity can't be published on <span class=\"brand\">keys."
"openpgp.org</span>. (<a href=\"/about/faq#non-email-uids\" target=\"_blank"
"\">Why?</a>)"
msgstr ""
"该密钥包含一个不能解析为邮件地址的身份。 <br />该身份无法在<span class="
"\"brand\">keys.openpgp.org</span>上发布。 <a href=\"/about/faq#non-email-"
"uids\" target=\"_blank\">为什么?</a>"
#: src/gettext_strings.rs:48
msgid ""
"This key contains {{ count_unparsed }} identities that could not be parsed "
"as an email address.<br /> These identities can't be published on <span "
"class=\"brand\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-email-"
"uids\" target=\"_blank\">Why?</a>)"
msgstr ""
"该密钥包含一个不能解析为邮件地址的身份{{ count_unparsed }} 。 <br />这些身份"
"无法在<span class=\"brand\">keys.openpgp.org</span>上发布。 <a href=\"/"
"about/faq#non-email-uids\" target=\"_blank\">为什么?</a>"
#: src/gettext_strings.rs:49
msgid ""
"This key contains one revoked identity, which is not published. (<a href=\"/"
"about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
msgstr ""
"该密钥包含一个被吊销的身份,所以没有发布(<a href=\"/about/faq#revoked-uids"
"\" target=\"_blank\">为什么?</a>"
#: src/gettext_strings.rs:50
msgid ""
"This key contains {{ count_revoked }} revoked identities, which are not "
"published. (<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Why?</a>)"
msgstr ""
"该密钥包含 {{ count_revoked }}个被吊销的身份,所以没有发布(<a href=\"/about/"
"faq#revoked-uids\" target=\"_blank\">为什么?</a>"
#: src/gettext_strings.rs:51
msgid "Your keys have been successfully uploaded:"
msgstr "你的密钥已成功上传"
#: src/gettext_strings.rs:52
msgid ""
"<strong>Note:</strong> To make keys searchable by email address, you must "
"upload them individually."
msgstr ""
"<strong>提示:</strong>如果想让一些密钥可以通过邮箱地址搜索到,你必须把这些密"
"钥逐个上传。"
#: src/gettext_strings.rs:53
msgid "Verifying your email address…"
msgstr "正在验证你的邮箱地址……"
#: src/gettext_strings.rs:54
msgid ""
"If the process doesn't complete after a few seconds, please <input type="
"\"submit\" class=\"textbutton\" value=\"click here\" />."
msgstr ""
"如果这项操作在若干秒后仍没有完成,<input type=\"submit\" class=\"textbutton"
"\" value=\"click here\" />。"
#: src/gettext_strings.rs:56
msgid "Manage your key on {{domain}}"
msgstr "在{{domain}}上管理你的密钥"
#: src/gettext_strings.rs:58
msgid "Hi,"
msgstr "你好,"
#: src/gettext_strings.rs:59
msgid ""
"This is an automated message from <a href=\"{{base_uri}}\" style=\"text-"
"decoration:none; color: #333\">{{domain}}</a>."
msgstr ""
"这是来自<a href=\"{{base_uri}}\" style=\"text-decoration:none; color: "
"#333\">{{domain}}</a>的一条自动信息"
#: src/gettext_strings.rs:60
msgid "If you didn't request this message, please ignore it."
msgstr "如果你没有申请过这条信息,请忽略。"
#: src/gettext_strings.rs:61
msgid "OpenPGP key: <tt>{{primary_fp}}</tt>"
msgstr "OpenPGP密钥<tt>{{primary_fp}}</tt>"
#: src/gettext_strings.rs:62
msgid ""
"To manage and delete listed addresses on this key, please follow the link "
"below:"
msgstr "要管理和删除此密钥上列出的地址,请点击以下链接:"
#: src/gettext_strings.rs:63
msgid ""
"You can find more info at <a href=\"{{base_uri}}/about\">{{domain}}/about</"
"a>."
msgstr ""
"你可以在以下链接找到更多信息 <a href=\"{{base_uri}}/about\">{{domain}}/"
"about</a>。"
#: src/gettext_strings.rs:64
msgid "distributing OpenPGP keys since 2019"
msgstr "自2019年起传播 OpenPGP 密钥"
#: src/gettext_strings.rs:67
msgid "This is an automated message from {{domain}}."
msgstr "这是一条来自 {{domain}} 的自动信息。"
#: src/gettext_strings.rs:69
msgid "OpenPGP key: {{primary_fp}}"
msgstr "OpenPGP密钥{{primary_fp}}"
#: src/gettext_strings.rs:71
msgid "You can find more info at {{base_uri}}/about"
msgstr "你可以在以下链接找到更多信息 {{base_uri}}/about"
#: src/gettext_strings.rs:74
msgid "Verify {{userid}} for your key on {{domain}}"
msgstr "在 {{domain}} 上验证你的密钥 {{userid}}"
#: src/gettext_strings.rs:80
msgid ""
"To let others find this key from your email address \"<a rel=\"nofollow\" "
"href=\"#\" style=\"text-decoration:none; color: #333\">{{userid}}</a>\", "
"please click the link below:"
msgstr ""
"要让其他人能通过你的邮件地址找到此密钥\"<a rel=\"nofollow\" href=\"#\" style="
"\"text-decoration:none; color: #333\">{{userid}}</a>\",请点击下方的链接:"
#: src/gettext_strings.rs:88
msgid ""
"To let others find this key from your email address \"{{userid}}\",\n"
"please follow the link below:"
msgstr ""
"要让其他人能通过你的邮件地址找到此密钥\"{{userid}}\"\n"
"请点击下方的链接:"
#: src/web/manage.rs:103
msgid "This link is invalid or expired"
msgstr "此链接无效或者已经过期"
#: src/web/manage.rs:129
msgid "Malformed address: {address}"
msgstr "地址格式错误:{address}"
#: src/web/manage.rs:136
msgid "No key for address: {address}"
msgstr "此地址下没有密钥:{address}"
#: src/web/manage.rs:152
msgid "A request has already been sent for this address recently."
msgstr "对此地址的申请已经发送"
#: src/web/vks.rs:112
msgid "Parsing of key data failed."
msgstr "密钥数据解析失败。"
#: src/web/vks.rs:121
msgid "Whoops, please don't upload secret keys!"
msgstr "我的天!请不要上传私钥!"
#: src/web/vks.rs:134
msgid "No key uploaded."
msgstr "没有密钥被上传"
#: src/web/vks.rs:178
msgid "Error processing uploaded key."
msgstr "处理已上传的密钥时出现问题"
#: src/web/vks.rs:248
msgid "Upload session expired. Please try again."
msgstr "上传会话过期,请重试。"
#: src/web/vks.rs:285
msgid "Invalid verification link."
msgstr "无效的验证链接"

90
src/anonymize_utils.rs Normal file
View File

@@ -0,0 +1,90 @@
use lazy_static::lazy_static;
use std::collections::HashSet;
use crate::database::types::Email;
// from https://github.com/mailcheck/mailcheck/wiki/List-of-Popular-Domains
lazy_static! {
static ref POPULAR_DOMAINS: HashSet<&'static str> = vec!(
/* Default domains included */
"aol.com", "att.net", "comcast.net", "facebook.com", "gmail.com", "gmx.com", "googlemail.com",
"google.com", "hotmail.com", "hotmail.co.uk", "mac.com", "me.com", "mail.com", "msn.com",
"live.com", "sbcglobal.net", "verizon.net", "yahoo.com", "yahoo.co.uk",
/* Other global domains */
"email.com", "fastmail.fm", "games.com" /* AOL */, "gmx.net", "hush.com", "hushmail.com", "icloud.com",
"iname.com", "inbox.com", "lavabit.com", "love.com" /* AOL */, "mailbox.org", "posteo.de", "outlook.com", "pobox.com", "protonmail.ch", "protonmail.com", "tutanota.de", "tutanota.com", "tutamail.com", "tuta.io",
"keemail.me", "rocketmail.com" /* Yahoo */, "safe-mail.net", "wow.com" /* AOL */, "ygm.com" /* AOL */,
"ymail.com" /* Yahoo */, "zoho.com", "yandex.com",
/* United States ISP domains */
"bellsouth.net", "charter.net", "cox.net", "earthlink.net", "juno.com",
/* British ISP domains */
"btinternet.com", "virginmedia.com", "blueyonder.co.uk", "freeserve.co.uk", "live.co.uk",
"ntlworld.com", "o2.co.uk", "orange.net", "sky.com", "talktalk.co.uk", "tiscali.co.uk",
"virgin.net", "wanadoo.co.uk", "bt.com",
/* Domains used in Asia */
"sina.com", "sina.cn", "qq.com", "naver.com", "hanmail.net", "daum.net", "nate.com", "yahoo.co.jp", "yahoo.co.kr", "yahoo.co.id", "yahoo.co.in", "yahoo.com.sg", "yahoo.com.ph", "163.com", "yeah.net", "126.com", "21cn.com", "aliyun.com", "foxmail.com",
/* French ISP domains */
"hotmail.fr", "live.fr", "laposte.net", "yahoo.fr", "wanadoo.fr", "orange.fr", "gmx.fr", "sfr.fr", "neuf.fr", "free.fr",
/* German ISP domains */
"gmx.de", "hotmail.de", "live.de", "online.de", "t-online.de" /* T-Mobile */, "web.de", "yahoo.de",
/* Italian ISP domains */
"libero.it", "virgilio.it", "hotmail.it", "aol.it", "tiscali.it", "alice.it", "live.it", "yahoo.it", "email.it", "tin.it", "poste.it", "teletu.it",
/* Russian ISP domains */
"mail.ru", "rambler.ru", "yandex.ru", "ya.ru", "list.ru",
/* Belgian ISP domains */
"hotmail.be", "live.be", "skynet.be", "voo.be", "tvcablenet.be", "telenet.be",
/* Argentinian ISP domains */
"hotmail.com.ar", "live.com.ar", "yahoo.com.ar", "fibertel.com.ar", "speedy.com.ar", "arnet.com.ar",
/* Domains used in Mexico */
"yahoo.com.mx", "live.com.mx", "hotmail.es", "hotmail.com.mx", "prodigy.net.mx",
/* Domains used in Brazil */
"yahoo.com.br", "hotmail.com.br", "outlook.com.br", "uol.com.br", "bol.com.br", "terra.com.br", "ig.com.br", "itelefonica.com.br", "r7.com", "zipmail.com.br", "globo.com", "globomail.com", "oi.com.br"
).into_iter().collect();
}
pub fn anonymize_address(email: &Email) -> Option<String> {
email.as_str()
.rsplit('@')
.next()
.map(|domain| domain.to_lowercase())
.and_then(|domain| {
if POPULAR_DOMAINS.contains(&domain.as_str()) {
Some(domain)
} else {
domain.rsplit('.').next().map(|tld| tld.to_owned())
}
})
}
pub fn anonymize_address_fallback(email: &Email) -> String {
anonymize_address(email).unwrap_or_else(|| "unknown".to_owned())
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn known_domain() {
let email = "user@hotmail.be".parse::<Email>().unwrap();
assert_eq!("hotmail.be", anonymize_address(&email).unwrap());
}
#[test]
fn unknown_domain() {
let email = "user@example.org".parse::<Email>().unwrap();
assert_eq!("org", anonymize_address(&email).unwrap());
}
}

67
src/counters.rs Normal file
View File

@@ -0,0 +1,67 @@
use lazy_static::lazy_static;
use rocket_prometheus::prometheus;
use crate::anonymize_utils;
use crate::database::types::Email;
lazy_static! {
static ref KEY_UPLOAD: LabelCounter =
LabelCounter::new("hagrid_key_upload", "Uploaded keys", &["result"]);
static ref MAIL_SENT: LabelCounter =
LabelCounter::new("hagrid_mail_sent", "Sent verification mails", &["type", "domain"]);
static ref KEY_ADDRESS_PUBLISHED: LabelCounter =
LabelCounter::new("hagrid_key_address_published", "Verified email addresses", &["domain"]);
static ref KEY_ADDRESS_UNPUBLISHED: LabelCounter =
LabelCounter::new("hagrid_key_address_unpublished", "Unpublished email addresses", &["domain"]);
}
pub fn register_counters(registry: &prometheus::Registry) {
KEY_UPLOAD.register(registry);
MAIL_SENT.register(registry);
KEY_ADDRESS_PUBLISHED.register(registry);
KEY_ADDRESS_UNPUBLISHED.register(registry);
}
pub fn inc_key_upload(upload_result: &str) {
KEY_UPLOAD.inc(&[upload_result]);
}
pub fn inc_mail_sent(mail_type: &str, email: &Email) {
let anonymized_adddress = anonymize_utils::anonymize_address_fallback(email);
MAIL_SENT.inc(&[mail_type, &anonymized_adddress]);
}
pub fn inc_address_published(email: &Email) {
let anonymized_adddress = anonymize_utils::anonymize_address_fallback(email);
KEY_ADDRESS_PUBLISHED.inc(&[&anonymized_adddress]);
}
pub fn inc_address_unpublished(email: &Email) {
let anonymized_adddress = anonymize_utils::anonymize_address_fallback(email);
KEY_ADDRESS_UNPUBLISHED.inc(&[&anonymized_adddress]);
}
struct LabelCounter {
prometheus_counter: prometheus::IntCounterVec,
}
impl LabelCounter {
fn new(name: &str, help: &str, labels: &[&str]) -> Self {
let opts = prometheus::Opts::new(name, help);
let prometheus_counter = prometheus::IntCounterVec::new(opts, labels).unwrap();
Self { prometheus_counter }
}
fn register(&self, registry: &prometheus::Registry) {
registry.register(Box::new(self.prometheus_counter.clone())).unwrap();
}
fn inc(&self, values: &[&str]) {
self.prometheus_counter.with_label_values(values).inc();
}
}

View File

@@ -9,7 +9,7 @@ extern crate structopt;
use structopt::StructOpt;
extern crate hagrid_database as database;
use database::{Query, Database, KeyDatabase};
use crate::database::{Query, Database, KeyDatabase};
#[derive(Debug, StructOpt)]
#[structopt(

View File

@@ -1,16 +1,18 @@
// from https://gitlab.com/sequoia-pgp/dump.sequoia-pgp.org/blob/master/src/dump.rs
// plus *very* slight adaptions for seqoia 0.8
// from https://gitlab.com/sequoia-pgp/sequoia/blob/master/tool/src/commands/dump.rs
use std::io::{self, Read};
use time;
use sequoia_openpgp::constants::SymmetricAlgorithm;
use sequoia_openpgp::conversions::hex;
use sequoia_openpgp::{Packet, Result};
use sequoia_openpgp::packet::ctb::CTB;
use sequoia_openpgp::packet::{Header, BodyLength, Signature};
use sequoia_openpgp::packet::signature::subpacket::{Subpacket, SubpacketValue};
use sequoia_openpgp::crypto::{SessionKey, s2k::S2K};
use sequoia_openpgp::parse::{map::Map, Parse, PacketParserResult};
extern crate sequoia_openpgp as openpgp;
use self::openpgp::constants::SymmetricAlgorithm;
use self::openpgp::conversions::hex;
use self::openpgp::crypto::mpis;
use self::openpgp::{Packet, Result};
use self::openpgp::packet::ctb::CTB;
use self::openpgp::packet::{Header, BodyLength, Signature};
use self::openpgp::packet::signature::subpacket::{Subpacket, SubpacketValue};
use self::openpgp::crypto::{SessionKey, s2k::S2K};
use self::openpgp::parse::{map::Map, Parse, PacketParserResult};
const TIMEFMT: &'static str = "%Y-%m-%dT%H:%M";
@@ -24,14 +26,17 @@ pub enum Kind {
Unknown,
}
pub fn dump(input: &mut dyn io::Read, output: &mut dyn io::Write, mpis: bool, hex: bool,
sk: Option<&SessionKey>)
-> Result<Kind> {
pub fn dump<W>(input: &mut dyn io::Read, output: &mut dyn io::Write,
mpis: bool, hex: bool, sk: Option<&SessionKey>,
width: W)
-> Result<Kind>
where W: Into<Option<usize>>
{
let mut ppr
= sequoia_openpgp::parse::PacketParserBuilder::from_reader(input)?
= self::openpgp::parse::PacketParserBuilder::from_reader(input)?
.map(hex).finalize()?;
let mut message_encrypted = false;
let width = 32 * 4 + 80;
let width = width.into().unwrap_or(80);
let mut dumper = PacketDumper::new(width, mpis);
while let PacketParserResult::Some(mut pp) = ppr {
@@ -232,7 +237,7 @@ impl PacketDumper {
header: Option<&Header>, p: &Packet, map: Option<&Map>,
additional_fields: Option<&Vec<String>>)
-> Result<()> {
use sequoia_openpgp::Packet::*;
use self::openpgp::Packet::*;
if let Some(h) = header {
write!(output, "{} CTB, {}: ",
@@ -283,40 +288,40 @@ impl PacketDumper {
level <= {} and data)", n - 1)?,
}
if self.mpis {
use sequoia_openpgp::crypto::mpis::Signature::*;
writeln!(output, "{}", i)?;
writeln!(output, "{} Signature:", i)?;
let ii = format!("{} ", i);
match s.mpis() {
RSA { s } =>
mpis::Signature::RSA { s } =>
self.dump_mpis(output, &ii,
&[&s.value],
&[s.value()],
&["s"])?,
DSA { r, s } =>
mpis::Signature::DSA { r, s } =>
self.dump_mpis(output, &ii,
&[&r.value, &s.value],
&[r.value(), s.value()],
&["r", "s"])?,
Elgamal { r, s } =>
mpis::Signature::Elgamal { r, s } =>
self.dump_mpis(output, &ii,
&[&r.value, &s.value],
&[r.value(), s.value()],
&["r", "s"])?,
EdDSA { r, s } =>
mpis::Signature::EdDSA { r, s } =>
self.dump_mpis(output, &ii,
&[&r.value, &s.value],
&[r.value(), s.value()],
&["r", "s"])?,
ECDSA { r, s } =>
mpis::Signature::ECDSA { r, s } =>
self.dump_mpis(output, &ii,
&[&r.value, &s.value],
&[r.value(), s.value()],
&["r", "s"])?,
Unknown { mpis, rest } => {
mpis::Signature::Unknown { mpis, rest } => {
let keys: Vec<String> =
(0..mpis.len()).map(
|i| format!("mpi{}", i)).collect();
self.dump_mpis(
output, &ii,
&mpis.iter().map(|m| m.value.iter().as_slice())
.collect::<Vec<_>>()[..],
&mpis.iter().map(|m| {
m.value().iter().as_slice()
}).collect::<Vec<_>>()[..],
&keys.iter().map(|k| k.as_str())
.collect::<Vec<_>>()[..],
)?;
@@ -349,48 +354,48 @@ impl PacketDumper {
writeln!(output, "{} Pk size: {} bits", i, bits)?;
}
if self.mpis {
use sequoia_openpgp::crypto::mpis::PublicKey::*;
writeln!(output, "{}", i)?;
writeln!(output, "{} Public Key:", i)?;
let ii = format!("{} ", i);
match k.mpis() {
RSA { e, n } =>
mpis::PublicKey::RSA { e, n } =>
self.dump_mpis(output, &ii,
&[&e.value, &n.value],
&[e.value(), n.value()],
&["e", "n"])?,
DSA { p, q, g, y } =>
mpis::PublicKey::DSA { p, q, g, y } =>
self.dump_mpis(output, &ii,
&[&p.value, &q.value, &g.value,
&y.value],
&[p.value(), q.value(), g.value(),
y.value()],
&["p", "q", "g", "y"])?,
Elgamal { p, g, y } =>
mpis::PublicKey::Elgamal { p, g, y } =>
self.dump_mpis(output, &ii,
&[&p.value, &g.value, &y.value],
&[p.value(), g.value(), y.value()],
&["p", "g", "y"])?,
EdDSA { curve, q } => {
mpis::PublicKey::EdDSA { curve, q } => {
writeln!(output, "{} Curve: {}", ii, curve)?;
self.dump_mpis(output, &ii, &[&q.value], &["q"])?;
self.dump_mpis(output, &ii, &[q.value()], &["q"])?;
},
ECDSA { curve, q } => {
mpis::PublicKey::ECDSA { curve, q } => {
writeln!(output, "{} Curve: {}", ii, curve)?;
self.dump_mpis(output, &ii, &[&q.value], &["q"])?;
self.dump_mpis(output, &ii, &[q.value()], &["q"])?;
},
ECDH { curve, q, hash, sym } => {
mpis::PublicKey::ECDH { curve, q, hash, sym } => {
writeln!(output, "{} Curve: {}", ii, curve)?;
writeln!(output, "{} Hash algo: {}", ii, hash)?;
writeln!(output, "{} Symmetric algo: {}", ii,
sym)?;
self.dump_mpis(output, &ii, &[&q.value], &["q"])?;
self.dump_mpis(output, &ii, &[q.value()], &["q"])?;
},
Unknown { mpis, rest } => {
mpis::PublicKey::Unknown { mpis, rest } => {
let keys: Vec<String> =
(0..mpis.len()).map(
|i| format!("mpi{}", i)).collect();
self.dump_mpis(
output, &ii,
&mpis.iter().map(|m| m.value.iter().as_slice())
.collect::<Vec<_>>()[..],
&mpis.iter().map(|m| {
m.value().iter().as_slice()
}).collect::<Vec<_>>()[..],
&keys.iter().map(|k| k.as_str())
.collect::<Vec<_>>()[..],
)?;
@@ -400,44 +405,46 @@ impl PacketDumper {
}
if let Some(secrets) = k.secret() {
use sequoia_openpgp::crypto::mpis::SecretKey::*;
use self::openpgp::packet::key::SecretKey;
writeln!(output, "{}", i)?;
writeln!(output, "{} Secret Key:", i)?;
let ii = format!("{} ", i);
match secrets {
sequoia_openpgp::packet::key::SecretKey::Unencrypted {
mpis,
} => match mpis {
RSA { d, p, q, u } =>
SecretKey::Unencrypted(ref u) => match u.mpis()
{
mpis::SecretKey::RSA { d, p, q, u } =>
self.dump_mpis(output, &ii,
&[&d.value, &p.value, &q.value,
&u.value],
&[d.value(), p.value(),
q.value(), u.value()],
&["d", "p", "q", "u"])?,
DSA { x } =>
self.dump_mpis(output, &ii, &[&x.value],
mpis::SecretKey::DSA { x } =>
self.dump_mpis(output, &ii, &[x.value()],
&["x"])?,
Elgamal { x } =>
self.dump_mpis(output, &ii, &[&x.value],
mpis::SecretKey::Elgamal { x } =>
self.dump_mpis(output, &ii, &[x.value()],
&["x"])?,
EdDSA { scalar } =>
self.dump_mpis(output, &ii, &[&scalar.value],
mpis::SecretKey::EdDSA { scalar } =>
self.dump_mpis(output, &ii,
&[scalar.value()],
&["scalar"])?,
ECDSA { scalar } =>
self.dump_mpis(output, &ii, &[&scalar.value],
mpis::SecretKey::ECDSA { scalar } =>
self.dump_mpis(output, &ii,
&[scalar.value()],
&["scalar"])?,
ECDH { scalar } =>
self.dump_mpis(output, &ii, &[&scalar.value],
mpis::SecretKey::ECDH { scalar } =>
self.dump_mpis(output, &ii,
&[scalar.value()],
&["scalar"])?,
Unknown { mpis, rest } => {
mpis::SecretKey::Unknown { mpis, rest } => {
let keys: Vec<String> =
(0..mpis.len()).map(
|i| format!("mpi{}", i)).collect();
self.dump_mpis(
output, &ii,
&mpis.iter()
.map(|m| m.value.iter().as_slice())
.collect::<Vec<_>>()[..],
&mpis.iter().map(|m| {
m.value().iter().as_slice()
}).collect::<Vec<_>>()[..],
&keys.iter().map(|k| k.as_str())
.collect::<Vec<_>>()[..],
)?;
@@ -446,15 +453,13 @@ impl PacketDumper {
&["rest"])?;
},
},
sequoia_openpgp::packet::key::SecretKey::Encrypted {
s2k, algorithm, ciphertext,
} => {
SecretKey::Encrypted(ref e) => {
writeln!(output, "{}", i)?;
write!(output, "{} S2K: ", ii)?;
self.dump_s2k(output, &ii, s2k)?;
self.dump_s2k(output, &ii, e.s2k())?;
writeln!(output, "{} Sym. algo: {}", ii,
algorithm)?;
self.dump_mpis(output, &ii, &[&ciphertext[..]],
e.algo())?;
self.dump_mpis(output, &ii, &[e.ciphertext()],
&["ciphertext"])?;
},
}
@@ -474,7 +479,7 @@ impl PacketDumper {
},
UserAttribute(ref u) => {
use sequoia_openpgp::packet::user_attribute::{Subpacket, Image};
use self::openpgp::packet::user_attribute::{Subpacket, Image};
writeln!(output, "User Attribute Packet")?;
for subpacket in u.subpackets() {
@@ -532,32 +537,32 @@ impl PacketDumper {
writeln!(output, "{} Recipient: {}", i, p.recipient())?;
writeln!(output, "{} Pk algo: {}", i, p.pk_algo())?;
if self.mpis {
use sequoia_openpgp::crypto::mpis::Ciphertext::*;
writeln!(output, "{}", i)?;
writeln!(output, "{} Encrypted session key:", i)?;
let ii = format!("{} ", i);
match p.esk() {
RSA { c } =>
mpis::Ciphertext::RSA { c } =>
self.dump_mpis(output, &ii,
&[&c.value],
&[c.value()],
&["c"])?,
Elgamal { e, c } =>
mpis::Ciphertext::Elgamal { e, c } =>
self.dump_mpis(output, &ii,
&[&e.value, &c.value],
&[e.value(), c.value()],
&["e", "c"])?,
ECDH { e, key } =>
mpis::Ciphertext::ECDH { e, key } =>
self.dump_mpis(output, &ii,
&[&e.value, key],
&[e.value(), key],
&["e", "key"])?,
Unknown { mpis, rest } => {
mpis::Ciphertext::Unknown { mpis, rest } => {
let keys: Vec<String> =
(0..mpis.len()).map(
|i| format!("mpi{}", i)).collect();
self.dump_mpis(
output, &ii,
&mpis.iter().map(|m| m.value.iter().as_slice())
.collect::<Vec<_>>()[..],
&mpis.iter().map(|m| {
m.value().iter().as_slice()
}).collect::<Vec<_>>()[..],
&keys.iter().map(|k| k.as_str())
.collect::<Vec<_>>()[..],
)?;
@@ -572,7 +577,7 @@ impl PacketDumper {
writeln!(output, "Symmetric-key Encrypted Session Key Packet")?;
writeln!(output, "{} Version: {}", i, s.version())?;
match s {
sequoia_openpgp::packet::SKESK::V4(ref s) => {
self::openpgp::packet::SKESK::V4(ref s) => {
writeln!(output, "{} Symmetric algo: {}", i,
s.symmetric_algo())?;
write!(output, "{} S2K: ", i)?;
@@ -583,7 +588,7 @@ impl PacketDumper {
}
},
sequoia_openpgp::packet::SKESK::V5(ref s) => {
self::openpgp::packet::SKESK::V5(ref s) => {
writeln!(output, "{} Symmetric algo: {}", i,
s.symmetric_algo())?;
writeln!(output, "{} AEAD: {}", i,
@@ -771,11 +776,11 @@ impl PacketDumper {
writeln!(output, "{} Hash: {}", i, hash)?;
writeln!(output, "{} Salt: {}", i, hex::encode(salt))?;
},
Iterated { hash, ref salt, .. } => {
Iterated { hash, ref salt, hash_bytes } => {
writeln!(output, "Iterated")?;
writeln!(output, "{} Hash: {}", i, hash)?;
writeln!(output, "{} Salt: {}", i, hex::encode(salt))?;
// writeln!(output, "{} Iterations: {}", i, iterations)?;
writeln!(output, "{} Hash bytes: {}", i, hash_bytes)?;
},
Private(n) =>
writeln!(output, "Private({})", n)?,

91
src/gettext_strings.rs Normal file
View File

@@ -0,0 +1,91 @@
use gettext_macros::t;
fn _dummy() {
t!("Error");
t!("Looks like something went wrong :(");
t!("Error message: {{ internal_error }}");
t!("There was an error with your request:");
t!("We found an entry for <span class=\"email\">{{ query }}</span>:");
t!("<strong>Hint:</strong> It's more convenient to use <span class=\"brand\">keys.openpgp.org</span> from your OpenPGP software.<br /> Take a look at our <a href=\"/about/usage\">usage guide</a> for details.");
t!("debug info");
t!("Search by Email Address / Key ID / Fingerprint");
t!("Search");
t!("You can also <a href=\"/upload\">upload</a> or <a href=\"/manage\">manage</a> your key.");
t!("Find out more <a href=\"/about\">about this service</a>.");
t!("News:");
t!("<a href=\"/about/news#2019-09-12-three-months-later\">Three months after launch ✨</a> (2019-09-12)");
t!("v{{ version }} built from");
t!("Powered by <a href=\"https://sequoia-pgp.org\">Sequoia-PGP</a>");
t!("Background image retrieved from <a href=\"https://www.toptal.com/designers/subtlepatterns/subtle-grey/\">Subtle Patterns</a> under CC BY-SA 3.0");
t!("Maintenance Mode");
t!("Manage your key");
t!("Enter any verified email address for your key");
t!("Send link");
t!("We will send you an email with a link you can use to remove any of your email addresses from search.");
t!("Managing the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" target=\"_blank\">{{ key_fpr }}</a></span>.");
t!("Your key is published with the following identity information:");
t!("Delete");
t!("Clicking \"delete\" on any address will remove it from this key. It will no longer appear in a search.<br /> To add another address, <a href=\"/upload\">upload</a> the key again.");
t!("Your key is published as only non-identity information. (<a href=\"/about\" target=\"_blank\">What does this mean?</a>)");
t!("To add an address, <a href=\"/upload\">upload</a> the key again.");
t!("We have sent an email with further instructions to <span class=\"email\">{{ address }}</span>.");
t!("This address has already been verified.");
t!("Your key <span class=\"fingerprint\">{{ key_fpr }}</span> is now published for the identity <a href=\"{{userid_link}}\" target=\"_blank\"><span class=\"email\">{{ userid }}</span></a>.");
t!("Upload your key");
t!("Upload");
t!("Need more info? Check our <a target=\"_blank\" href=\"/about\">intro</a> and <a target=\"_blank\" href=\"/about/usage\">usage guide</a>.");
t!("You uploaded the key <span class=\"fingerprint\"><a href=\"{{ key_link }}\" target=\"_blank\">{{ key_fpr }}</a></span>.");
t!("This key is revoked.");
t!("It is published without identity information and can't be made available for search by email address (<a href=\"/about\" target=\"_blank\">what does this mean?</a>).");
t!("This key is now published with the following identity information (<a href=\"/about\" target=\"_blank\">what does this mean?</a>):");
t!("Published");
t!("This key is now published with only non-identity information. (<a href=\"/about\" target=\"_blank\">What does this mean?</a>)");
t!("To make the key available for search by email address, you can verify it belongs to you:");
t!("Verification Pending");
t!("<strong>Note:</strong> Some providers delay emails for up to 15 minutes to prevent spam. Please be patient.");
t!("Send Verification Email");
t!("This key contains one identity that could not be parsed as an email address.<br /> This identity can't be published on <span class=\"brand\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-email-uids\" target=\"_blank\">Why?</a>)");
t!("This key contains {{ count_unparsed }} identities that could not be parsed as an email address.<br /> These identities can't be published on <span class=\"brand\">keys.openpgp.org</span>. (<a href=\"/about/faq#non-email-uids\" target=\"_blank\">Why?</a>)");
t!("This key contains one revoked identity, which is not published. (<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Why?</a>)");
t!("This key contains {{ count_revoked }} revoked identities, which are not published. (<a href=\"/about/faq#revoked-uids\" target=\"_blank\">Why?</a>)");
t!("Your keys have been successfully uploaded:");
t!("<strong>Note:</strong> To make keys searchable by email address, you must upload them individually.");
t!("Verifying your email address…");
t!("If the process doesn't complete after a few seconds, please <input type=\"submit\" class=\"textbutton\" value=\"click here\" />.");
t!("Manage your key on {{domain}}");
t!("Hi,");
t!("This is an automated message from <a href=\"{{base_uri}}\" style=\"text-decoration:none; color: #333\">{{domain}}</a>.");
t!("If you didn't request this message, please ignore it.");
t!("OpenPGP key: <tt>{{primary_fp}}</tt>");
t!("To manage and delete listed addresses on this key, please follow the link below:");
t!("You can find more info at <a href=\"{{base_uri}}/about\">{{domain}}/about</a>.");
t!("distributing OpenPGP keys since 2019");
t!("Hi,");
t!("This is an automated message from {{domain}}.");
t!("If you didn't request this message, please ignore it.");
t!("OpenPGP key: {{primary_fp}}");
t!("To manage and delete listed addresses on this key, please follow the link below:");
t!("You can find more info at {{base_uri}}/about");
t!("distributing OpenPGP keys since 2019");
t!("Verify {{userid}} for your key on {{domain}}");
t!("Hi,");
t!("This is an automated message from <a href=\"{{base_uri}}\" style=\"text-decoration:none; color: #333\">{{domain}}</a>.");
t!("If you didn't request this message, please ignore it.");
t!("OpenPGP key: <tt>{{primary_fp}}</tt>");
t!("To let others find this key from your email address \"<a rel=\"nofollow\" href=\"#\" style=\"text-decoration:none; color: #333\">{{userid}}</a>\", please click the link below:");
t!("You can find more info at <a href=\"{{base_uri}}/about\">{{domain}}/about</a>.");
t!("distributing OpenPGP keys since 2019");
t!("Hi,");
t!("This is an automated message from {{domain}}.");
t!("If you didn't request this message, please ignore it.");
t!("OpenPGP key: {{primary_fp}}");
t!("To let others find this key from your email address \"{{userid}}\",\nplease follow the link below:");
t!("You can find more info at {{base_uri}}/about");
t!("distributing OpenPGP keys since 2019");
}

102
src/i18n.rs Normal file
View File

@@ -0,0 +1,102 @@
use handlebars::{
Context, Handlebars, Helper, HelperDef, HelperResult, Output, RenderContext, RenderError
};
use std::io;
pub struct I18NHelper {
catalogs: Vec<(&'static str, gettext::Catalog)>,
}
impl I18NHelper {
pub fn new(catalogs: Vec<(&'static str, gettext::Catalog)>) -> Self {
Self { catalogs }
}
pub fn get_catalog(
&self,
lang: &str,
) -> &gettext::Catalog {
let (_, ref catalog) = self.catalogs
.iter()
.find(|(candidate, _)| *candidate == lang)
.unwrap_or_else(|| self.catalogs.get(0).unwrap());
catalog
}
// Traverse the fallback chain,
pub fn lookup<'a>(
&'a self,
lang: &str,
text_id: &'a str,
// args: Option<&HashMap<&str, FluentValue>>,
) -> &'a str {
let catalog = self.get_catalog(lang);
catalog.gettext(text_id)
// format!("Unknown localization {}", text_id)
}
}
#[derive(Default)]
struct StringOutput {
pub s: String,
}
impl Output for StringOutput {
fn write(&mut self, seg: &str) -> Result<(), io::Error> {
self.s.push_str(seg);
Ok(())
}
}
impl HelperDef for I18NHelper {
fn call<'reg: 'rc, 'rc>(
&self,
h: &Helper<'reg, 'rc>,
reg: &'reg Handlebars,
context: &'rc Context,
rcx: &mut RenderContext<'reg>,
out: &mut dyn Output,
) -> HelperResult {
let id = if let Some(id) = h.param(0) {
id
} else {
return Err(RenderError::new(
"{{text}} must have at least one parameter",
));
};
let id = if let Some(id) = id.value().as_str() {
id
} else {
return Err(RenderError::new("{{text}} takes an identifier parameter"));
};
let rerender = h
.param(1)
.and_then(|p| p
.path()
.map(|v| v == "rerender")
).unwrap_or(false);
let lang = context
.data()
.get("lang")
.expect("Language not set in context")
.as_str()
.expect("Language must be string");
let response = self.lookup(lang, &id);
if rerender {
let data = rcx.evaluate(context, ".", false).unwrap();
let response = reg.render_template(&response, data)
.map_err(RenderError::with)?;
out.write(&response).map_err(RenderError::with)?;
} else {
out.write(&response).map_err(RenderError::with)?;
}
Ok(())
}
}

View File

@@ -7,13 +7,22 @@ use lettre_email::{Mailbox,EmailBuilder};
use url;
use serde::Serialize;
use uuid::Uuid;
use crate::counters;
use database::types::Email;
use Result;
use rocket_i18n::I18n;
use gettext_macros::i18n;
use rfc2047::rfc2047_encode;
use crate::template_helpers;
use crate::database::types::Email;
use crate::Result;
mod context {
#[derive(Serialize, Clone)]
pub struct Verification {
pub lang: String,
pub primary_fp: String,
pub uri: String,
pub userid: String,
@@ -23,6 +32,16 @@ mod context {
#[derive(Serialize, Clone)]
pub struct Manage {
pub lang: String,
pub primary_fp: String,
pub uri: String,
pub base_uri: String,
pub domain: String,
}
#[derive(Serialize, Clone)]
pub struct Welcome {
pub lang: String,
pub primary_fp: String,
pub uri: String,
pub base_uri: String,
@@ -44,37 +63,35 @@ enum Transport {
impl Service {
/// Sends mail via sendmail.
pub fn sendmail(from: String, base_uri: String, templates: Handlebars)
-> Result<Self> {
Self::new(from, base_uri, templates, Transport::Sendmail)
pub fn sendmail(from: String, base_uri: String, template_dir: PathBuf) -> Result<Self> {
Self::new(from, base_uri, template_dir, Transport::Sendmail)
}
/// Sends mail by storing it in the given directory.
pub fn filemail(from: String, base_uri: String, templates: Handlebars,
path: PathBuf)
-> Result<Self> {
Self::new(from, base_uri, templates, Transport::Filemail(path))
pub fn filemail(from: String, base_uri: String, template_dir: PathBuf, path: PathBuf) -> Result<Self> {
Self::new(from, base_uri, template_dir, Transport::Filemail(path))
}
fn new(from: String, base_uri: String, templates: Handlebars,
transport: Transport)
fn new(from: String, base_uri: String, template_dir: PathBuf, transport: Transport)
-> Result<Self> {
let templates = template_helpers::load_handlebars(template_dir)?;
let domain =
url::Url::parse(&base_uri)
?.host_str().ok_or_else(|| failure::err_msg("No host in base-URI"))
?.to_string();
Ok(Self {
from: from.parse().unwrap(),
domain: domain,
templates: templates,
transport: transport,
})
Ok(Self { from: from.parse().unwrap(), domain, templates, transport })
}
pub fn send_verification(&self, base_uri: &str, tpk_name: String, userid: &Email,
token: &str)
-> Result<()> {
pub fn send_verification(
&self,
i18n: &I18n,
base_uri: &str,
tpk_name: String,
userid: &Email,
token: &str
) -> Result<()> {
let ctx = context::Verification {
lang: i18n.lang.to_string(),
primary_fp: tpk_name,
uri: format!("{}/verify/{}", base_uri, token),
userid: userid.to_string(),
@@ -82,62 +99,119 @@ impl Service {
domain: self.domain.clone(),
};
counters::inc_mail_sent("verify", userid);
self.send(
&vec![userid],
&format!("Verify {} for your key on {}", userid, self.domain),
&i18n!(
i18n.catalog,
context = "Subject for verification email",
"Verify {userid} for your key on {domain}";
userid = userid,
domain = self.domain
),
"verify",
i18n.lang,
ctx,
)
}
pub fn send_manage_token(&self, base_uri: &str, tpk_name: String, recipient: &Email,
link_path: &str) -> Result<()> {
pub fn send_manage_token(
&self,
i18n: &I18n,
base_uri: &str,
tpk_name: String,
recipient: &Email,
link_path: &str,
) -> Result<()> {
let ctx = context::Manage {
lang: i18n.lang.to_string(),
primary_fp: tpk_name,
uri: format!("{}{}", base_uri, link_path),
base_uri: base_uri.to_owned(),
domain: self.domain.clone(),
};
counters::inc_mail_sent("manage", recipient);
self.send(
&[recipient],
&format!("Manage your key on {}", self.domain),
&i18n!(
i18n.catalog,
context = "Subject for manage email",
"Manage your key on {domain}";
domain = self.domain
),
"manage",
i18n.lang,
ctx,
)
}
fn send<T>(&self, to: &[&Email], subject: &str, template: &str, ctx: T)
-> Result<()>
where T: Serialize + Clone,
{
let tmpl_html = format!("{}-html", template);
let tmpl_txt = format!("{}-txt", template);
let (html, txt) = {
if let (Ok(inner_html), Ok(inner_txt)) = (
self.templates.render(&tmpl_html, &ctx),
self.templates.render(&tmpl_txt, &ctx),
) {
(Some(inner_html), Some(inner_txt))
} else {
(None, None)
}
pub fn send_welcome(
&self,
i18n: &I18n,
base_uri: &str,
tpk_name: String,
userid: &Email,
token: &str
) -> Result<()> {
let ctx = context::Welcome {
lang: i18n.lang.to_string(),
primary_fp: tpk_name,
uri: format!("{}/upload/{}", base_uri, token),
base_uri: base_uri.to_owned(),
domain: self.domain.clone(),
};
counters::inc_mail_sent("welcome", userid);
self.send(
&vec![userid],
&format!("Your key upload on {domain}", domain = self.domain),
"welcome",
i18n.lang,
ctx,
)
}
fn render_template(
&self,
template: &str,
locale: &str,
ctx: impl Serialize
) -> Result<(String, String)> {
let html = self.templates.render(&format!("{}/{}.htm", locale, template), &ctx)
.or_else(|_| self.templates.render(&format!("{}.htm", template), &ctx))
.map_err(|_| failure::err_msg("Email template failed to render"))?;
let txt = self.templates.render(&format!("{}/{}.txt", locale, template), &ctx)
.or_else(|_| self.templates.render(&format!("{}.txt", template), &ctx))
.map_err(|_| failure::err_msg("Email template failed to render"))?;
Ok((html, txt))
}
fn send(
&self,
to: &[&Email],
subject: &str,
template: &str,
locale: &str,
ctx: impl Serialize
) -> Result<()> {
let (html, txt) = self.render_template(template, locale, ctx)?;
if cfg!(debug_assertions) {
for recipient in to.iter() {
println!("To: {}", recipient.to_string());
}
println!("{}", txt.as_ref().unwrap().to_string());
println!("{}", &txt);
}
let email = EmailBuilder::new()
.from(self.from.clone())
.subject(subject)
.alternative(
html.ok_or(failure::err_msg("Email template failed to render"))?,
txt.ok_or(failure::err_msg("Email template failed to render"))?,
)
.subject(rfc2047_encode(subject))
.alternative(html, txt)
.message_id(format!("<{}@{}>", Uuid::new_v4(), self.domain));
let email = to.iter().fold(email, |email, to| email.to(to.to_string()));
@@ -158,3 +232,4 @@ impl Service {
Ok(())
}
}

View File

@@ -1,42 +1,36 @@
#![feature(proc_macro_hygiene, plugin, decl_macro)]
#![recursion_limit = "1024"]
extern crate failure;
use failure::Fallible as Result;
extern crate serde;
#[macro_use]
extern crate serde_derive;
extern crate serde_json;
extern crate time;
extern crate url;
#[macro_use]
extern crate rocket;
extern crate multipart;
#[macro_use]
extern crate rocket_contrib;
extern crate sequoia_openpgp;
extern crate handlebars;
extern crate lettre;
extern crate lettre_email;
extern crate tempfile;
extern crate uuid;
#[cfg(test)]
extern crate regex;
extern crate ring;
extern crate hagrid_database as database;
use gettext_macros::init_i18n;
init_i18n!("hagrid", en, de, fr, pl, tr, zh_Hans);
mod mail;
mod web;
mod anonymize_utils;
mod tokens;
mod sealed_state;
mod rate_limiter;
mod dump;
mod counters;
mod i18n;
mod gettext_strings;
mod web;
mod template_helpers;
fn main() {
if let Err(e) = web::serve() {
@@ -47,6 +41,7 @@ fn main() {
cause = c;
}
eprintln!();
::std::process::exit(2);
}
}

82
src/template_helpers.rs Normal file
View File

@@ -0,0 +1,82 @@
use std::path::{Path, PathBuf};
use std::collections::HashSet;
use handlebars::Handlebars;
use gettext_macros::include_i18n;
use crate::Result;
use crate::i18n::I18NHelper;
#[derive(Debug)]
pub struct TemplateOverrides(String, HashSet<(String)>);
impl TemplateOverrides {
pub fn load(template_path: &Path, localized_dir: &str) -> Result<Self> {
load_localized_template_names(template_path, localized_dir)
.map(|vec| Self(localized_dir.to_owned(), vec))
}
pub fn get_template_override(&self, lang: &str, tmpl: &str) -> Option<String> {
let template_name = format!("{}/{}/{}", self.0, lang, tmpl);
if self.1.contains(&template_name) {
println!("{}", &template_name);
Some(template_name)
} else {
None
}
}
}
fn load_localized_template_names(template_path: &Path, localized_dir: &str) -> Result<HashSet<(String)>> {
let language_glob = template_path.join(localized_dir).join("*");
glob::glob(language_glob.to_str().expect("valid glob path string"))
.unwrap()
.flatten()
.flat_map(|language_path| {
let mut template_glob = language_path.join("**").join("*");
template_glob.set_extension("hbs");
glob::glob(template_glob.to_str().expect("valid glob path string"))
.unwrap()
.flatten()
.map(move |path| {
// TODO this is a hack
let template_name = remove_extension(remove_extension(path.strip_prefix(&template_path)?));
Ok(template_name.to_string_lossy().into_owned())
})
})
.collect()
}
pub fn load_handlebars(template_dir: PathBuf) -> Result<Handlebars> {
let mut handlebars = Handlebars::new();
let i18ns = include_i18n!();
let i18n_helper = I18NHelper::new(i18ns);
handlebars.register_helper("text", Box::new(i18n_helper));
let mut glob_path = template_dir.join("**").join("*");
glob_path.set_extension("hbs");
let glob_path = glob_path.to_str().expect("valid glob path string");
for path in glob::glob(glob_path).unwrap().flatten() {
let template_name = remove_extension(path.strip_prefix(&template_dir)?);
handlebars.register_template_file(&template_name.to_string_lossy(), &path)?;
}
Ok(handlebars)
}
fn remove_extension<P: AsRef<Path>>(path: P) -> PathBuf {
let path = path.as_ref();
let stem = match path.file_stem() {
Some(stem) => stem,
None => return path.to_path_buf()
};
match path.parent() {
Some(parent) => parent.join(stem),
None => PathBuf::from(stem)
}
}

View File

@@ -1,8 +1,8 @@
use sealed_state::SealedState;
use crate::sealed_state::SealedState;
use serde_json;
use serde::{Serialize,de::DeserializeOwned};
use Result;
use crate::Result;
pub trait StatelessSerializable : Serialize + DeserializeOwned {
}

View File

@@ -1,9 +1,9 @@
use std::io;
use dump::{self, Kind};
use web::MyResponse;
use crate::dump::{self, Kind};
use crate::web::MyResponse;
use database::{Database, KeyDatabase, Query};
use crate::database::{Database, KeyDatabase, Query};
#[get("/debug?<q>")]
pub fn debug_info(
@@ -30,7 +30,9 @@ pub fn debug_info(
&mut result,
false,
false,
None);
None,
32 * 4 + 80,
);
match dump_result {
Ok(Kind::TPK) => {
match String::from_utf8(result) {

View File

@@ -5,16 +5,19 @@ use rocket::Outcome;
use rocket::http::{ContentType, Status};
use rocket::request::{self, Request, FromRequest};
use rocket::http::uri::Uri;
use rocket_i18n::I18n;
use database::{Database, Query, KeyDatabase};
use database::types::{Email, Fingerprint, KeyID};
use crate::database::{Database, Query, KeyDatabase};
use crate::database::types::{Email, Fingerprint, KeyID};
use rate_limiter::RateLimiter;
use crate::rate_limiter::RateLimiter;
use tokens;
use crate::tokens;
use web;
use web::{HagridState, RequestOrigin, MyResponse, vks_web};
use crate::web;
use crate::mail;
use crate::web::{HagridState, RequestOrigin, MyResponse, vks_web};
use crate::web::vks::response::UploadResponse;
#[derive(Debug)]
pub enum Hkp {
@@ -67,7 +70,9 @@ impl<'a, 'r> FromRequest<'a, 'r> for Hkp {
let maybe_fpr = Fingerprint::from_str(&search);
let maybe_keyid = KeyID::from_str(&search);
if search.starts_with("0x") && search.len() < 16 && !search.contains('@') {
let looks_like_short_key_id = !search.contains('@') &&
(search.starts_with("0x") && search.len() < 16 || search.len() == 8);
if looks_like_short_key_id {
Outcome::Success(Hkp::ShortKeyID {
query: search,
index: index,
@@ -113,10 +118,11 @@ pub fn pks_add_form_data(
db: rocket::State<KeyDatabase>,
tokens_stateless: rocket::State<tokens::Service>,
rate_limiter: rocket::State<RateLimiter>,
i18n: I18n,
cont_type: &ContentType,
data: Data,
) -> MyResponse {
match vks_web::upload_post_form_data(db, tokens_stateless, rate_limiter, cont_type, data) {
match vks_web::process_post_form_data(db, tokens_stateless, rate_limiter, i18n, cont_type, data) {
Ok(_) => MyResponse::plain("Ok".into()),
Err(err) => MyResponse::ise(err),
}
@@ -128,21 +134,49 @@ pub fn pks_add_form(
db: rocket::State<KeyDatabase>,
tokens_stateless: rocket::State<tokens::Service>,
rate_limiter: rocket::State<RateLimiter>,
mail_service: rocket::State<mail::Service>,
i18n: I18n,
data: Data,
) -> MyResponse {
match vks_web::upload_post_form(db, tokens_stateless, rate_limiter, data) {
match vks_web::process_post_form(&db, &tokens_stateless, &rate_limiter, &i18n, data) {
Ok(UploadResponse::Ok { is_new_key, key_fpr, primary_uid, token, .. }) => {
let msg = if is_new_key && send_welcome_mail(&request_origin, &mail_service, &i18n, key_fpr, primary_uid, token) {
format!("Upload successful. This is a new key, a welcome email has been sent.")
} else {
format!("Upload successful. Please note that identity information will only be published after verification. See {baseuri}/about/usage#gnupg-upload", baseuri = request_origin.get_base_uri())
};
MyResponse::plain(msg)
}
Ok(_) => {
let msg = format!("Upload successful. Note that identity information will only be published with verification! see {}/about/usage#gnupg-upload", request_origin.get_base_uri());
let msg = format!("Upload successful. Please note that identity information will only be published after verification. See {baseuri}/about/usage#gnupg-upload", baseuri = request_origin.get_base_uri());
MyResponse::plain(msg)
}
Err(err) => MyResponse::ise(err),
}
}
fn send_welcome_mail(
request_origin: &RequestOrigin,
mail_service: &mail::Service,
i18n: &I18n,
fpr: String,
primary_uid: Option<Email>,
token: String,
) -> bool {
if let Some(primary_uid) = primary_uid {
mail_service.send_welcome(
i18n, request_origin.get_base_uri(), fpr, &primary_uid, &token).is_ok()
} else {
false
}
}
#[get("/pks/lookup")]
pub fn pks_lookup(state: rocket::State<HagridState>,
pub fn pks_lookup(
state: rocket::State<HagridState>,
db: rocket::State<KeyDatabase>,
key: Hkp) -> MyResponse {
key: Hkp
) -> MyResponse {
let (query, index) = match key {
Hkp::Fingerprint { fpr, index } =>
(Query::ByFingerprint(fpr), index),
@@ -166,6 +200,17 @@ pub fn pks_lookup(state: rocket::State<HagridState>,
}
}
#[get("/pks/internal/index/<query_string>")]
pub fn pks_internal_index(
db: rocket::State<KeyDatabase>,
query_string: String,
) -> MyResponse {
match query_string.parse() {
Ok(query) => key_to_hkp_index(db, query),
Err(_) => MyResponse::bad_request_plain("Invalid search query!")
}
}
fn key_to_hkp_index(db: rocket::State<KeyDatabase>, query: Query)
-> MyResponse {
use sequoia_openpgp::RevocationStatus;
@@ -178,11 +223,7 @@ fn key_to_hkp_index(db: rocket::State<KeyDatabase>, query: Query)
let mut out = String::default();
let p = tpk.primary();
let ctime = tpk
.primary_key_signature()
.and_then(|x| x.signature_creation_time())
.map(|x| format!("{}", x.to_timespec().sec))
.unwrap_or_default();
let ctime = format!("{}", p.creation_time().to_timespec().sec);
let extime = tpk
.primary_key_signature()
.and_then(|x| x.signature_expiration_time())
@@ -258,7 +299,7 @@ mod tests {
use sequoia_openpgp::tpk::TPKBuilder;
use sequoia_openpgp::serialize::Serialize;
use web::tests::*;
use crate::web::tests::*;
#[test]
fn hkp() {
@@ -295,9 +336,9 @@ mod tests {
let body = response.body_string().unwrap();
eprintln!("response: {}", body);
// Check that we do not get a confirmation mail.
let confirm_mail = pop_mail(filemail_into.as_path()).unwrap();
assert!(confirm_mail.is_none());
// Check that we get a welcome mail
let welcome_mail = pop_mail(filemail_into.as_path()).unwrap();
assert!(welcome_mail.is_some());
// We should not be able to look it up by email address.
check_null_responses_by_email(&client, "foo@invalid.example.com");
@@ -309,6 +350,16 @@ mod tests {
// And check that we can see the human-readable result page.
check_hr_responses_by_fingerprint(&client, &tpk, 0);
// Upload the same key again, make sure the welcome mail is not sent again
let response = client.post("/pks/add")
.body(post_data.as_bytes())
.header(ContentType::Form)
.dispatch();
assert_eq!(response.status(), Status::Ok);
let welcome_mail = pop_mail(filemail_into.as_path()).unwrap();
assert!(welcome_mail.is_none());
assert_consistency(client.rocket());
}
@@ -345,8 +396,11 @@ mod tests {
.header(ContentType::Form)
.dispatch();
assert_eq!(response.status(), Status::Ok);
let confirm_mail = pop_mail(filemail_into.as_path()).unwrap();
assert!(confirm_mail.is_none());
// Check that there is no welcome mail (since we uploaded two)
let welcome_mail = pop_mail(filemail_into.as_path()).unwrap();
assert!(welcome_mail.is_none());
check_mr_responses_by_fingerprint(&client, &tpk_0, 0);
check_mr_responses_by_fingerprint(&client, &tpk_1, 0);
check_hr_responses_by_fingerprint(&client, &tpk_0, 0);

View File

@@ -1,12 +1,13 @@
use rocket::{Request, Data};
use rocket::fairing::{Fairing, Info, Kind};
use rocket_contrib::templates::Template;
use rocket::http::Method;
use rocket_contrib::templates::Template;
use rocket_i18n::I18n;
use std::fs;
use std::path::PathBuf;
use web::MyResponse;
use crate::web::MyResponse;
pub struct MaintenanceMode {
maintenance_file: PathBuf,
@@ -18,6 +19,7 @@ mod templates {
pub message: String,
pub commit: String,
pub version: String,
pub lang: String,
}
}
@@ -93,11 +95,15 @@ pub fn maintenance_error_json(message: String) -> MyResponse {
}
#[get("/maintenance/web/<message>")]
pub fn maintenance_error_web(message: String) -> MyResponse {
pub fn maintenance_error_web(
message: String,
i18n: I18n,
) -> MyResponse {
let ctx = templates::MaintenanceMode{
message,
version: env!("VERGEN_SEMVER").to_string(),
commit: env!("VERGEN_SHA_SHORT").to_string(),
lang: i18n.lang.to_owned(),
};
MyResponse::Maintenance(Template::render("maintenance", ctx))
}

View File

@@ -1,15 +1,19 @@
use rocket;
use rocket::State;
use rocket::request::Form;
use rocket_i18n::I18n;
use failure::Fallible as Result;
use web::{RequestOrigin, MyResponse, templates::General};
use web::vks_web;
use database::{Database, KeyDatabase, types::Email, types::Fingerprint};
use mail;
use rate_limiter::RateLimiter;
use tokens::{self, StatelessSerializable};
use gettext_macros::i18n;
use crate::web::{RequestOrigin, MyResponse};
use crate::web::vks_web;
use crate::database::{Database, KeyDatabase, types::Email, types::Fingerprint};
use crate::mail;
use crate::counters;
use crate::rate_limiter::RateLimiter;
use crate::tokens::{self, StatelessSerializable};
#[derive(Debug,Serialize,Deserialize)]
struct StatelessVerifyToken {
@@ -26,8 +30,6 @@ mod templates {
pub base_uri: String,
pub uid_status: Vec<ManageKeyUidStatus>,
pub token: String,
pub commit: String,
pub version: String,
}
#[derive(Serialize)]
@@ -57,17 +59,18 @@ pub mod forms {
#[get("/manage")]
pub fn vks_manage() -> Result<MyResponse> {
Ok(MyResponse::ok("manage/manage", General::default()))
Ok(MyResponse::ok_bare("manage/manage"))
}
#[get("/manage/<token>")]
pub fn vks_manage_key(
request_origin: RequestOrigin,
db: State<KeyDatabase>,
i18n: I18n,
token: String,
token_service: rocket::State<tokens::Service>,
) -> MyResponse {
use database::types::Fingerprint;
use crate::database::types::Fingerprint;
use std::convert::TryFrom;
if let Ok(StatelessVerifyToken { fpr }) = token_service.check(&token) {
match db.lookup(&database::Query::ByFingerprint(fpr)) {
@@ -92,20 +95,18 @@ pub fn vks_manage_key(
uid_status,
token,
base_uri: request_origin.get_base_uri().to_owned(),
version: env!("VERGEN_SEMVER").to_string(),
commit: env!("VERGEN_SHA_SHORT").to_string(),
};
MyResponse::ok("manage/manage_key", context)
},
Ok(None) => MyResponse::not_found(
Some("manage/manage"),
Some("This link is invalid or expired".to_owned())),
Some(i18n!(i18n.catalog, "This link is invalid or expired"))),
Err(e) => MyResponse::ise(e),
}
} else {
MyResponse::not_found(
Some("manage/manage"),
Some("This link is invalid or expired".to_owned()))
Some(i18n!(i18n.catalog, "This link is invalid or expired")))
}
}
@@ -115,6 +116,7 @@ pub fn vks_manage_post(
request_origin: RequestOrigin,
mail_service: rocket::State<mail::Service>,
rate_limiter: rocket::State<RateLimiter>,
i18n: I18n,
request: Form<forms::ManageRequest>,
token_service: rocket::State<tokens::Service>,
) -> MyResponse {
@@ -124,14 +126,14 @@ pub fn vks_manage_post(
Ok(email) => email,
Err(_) => return MyResponse::not_found(
Some("manage/manage"),
Some(format!("Malformed email address: {}", request.search_term)))
Some(i18n!(i18n.catalog, "Malformed address: {address}"; address = request.search_term)))
};
let tpk = match db.lookup(&database::Query::ByEmail(email.clone())) {
Ok(Some(tpk)) => tpk,
Ok(None) => return MyResponse::not_found(
Some("manage/manage"),
Some(format!("No key for address {}", request.search_term))),
Some(i18n!(i18n.catalog, "No key for address: {address}"; address = request.search_term))),
Err(e) => return MyResponse::ise(e),
};
@@ -140,13 +142,14 @@ pub fn vks_manage_post(
.any(|candidate| candidate == email);
if !email_exists {
return MyResponse::ise(failure::err_msg("Address check failed!"));
return MyResponse::ise(
failure::err_msg("Internal error: address check failed!"));
}
if !rate_limiter.action_perform(format!("manage-{}", &email)) {
return MyResponse::not_found(
Some("manage/manage"),
Some("A request was already sent for this address recently.".to_owned()));
Some(i18n!(i18n.catalog, "A request has already been sent for this address recently.")));
}
let fpr: Fingerprint = tpk.fingerprint().try_into().unwrap();
@@ -155,7 +158,7 @@ pub fn vks_manage_post(
let link_path = uri!(vks_manage_key: token).to_string();
let base_uri = request_origin.get_base_uri();
if let Err(e) = mail_service.send_manage_token(base_uri, fpr_text, &email, &link_path) {
if let Err(e) = mail_service.send_manage_token(&i18n, base_uri, fpr_text, &email, &link_path) {
return MyResponse::ise(e);
}
@@ -169,10 +172,11 @@ pub fn vks_manage_post(
pub fn vks_manage_unpublish(
request_origin: RequestOrigin,
db: rocket::State<KeyDatabase>,
i18n: I18n,
token_service: rocket::State<tokens::Service>,
request: Form<forms::ManageDelete>,
) -> MyResponse {
match vks_manage_unpublish_or_fail(request_origin, db, token_service, request) {
match vks_manage_unpublish_or_fail(request_origin, db, token_service, i18n, request) {
Ok(response) => response,
Err(e) => MyResponse::ise(e),
}
@@ -182,10 +186,14 @@ pub fn vks_manage_unpublish_or_fail(
request_origin: RequestOrigin,
db: rocket::State<KeyDatabase>,
token_service: rocket::State<tokens::Service>,
i18n: I18n,
request: Form<forms::ManageDelete>,
) -> Result<MyResponse> {
let verify_token = token_service.check::<StatelessVerifyToken>(&request.token)?;
let email = request.address.parse::<Email>()?;
db.set_email_unpublished(&verify_token.fpr, &email)?;
Ok(vks_manage_key(request_origin, db, request.token.to_owned(), token_service))
counters::inc_address_unpublished(&email);
Ok(vks_manage_key(request_origin, db, i18n, request.token.to_owned(), token_service))
}

View File

@@ -1,25 +1,33 @@
use rocket;
use rocket::http::Header;
use rocket::http::{Header, Status};
use rocket::request;
use rocket::outcome::Outcome;
use rocket::response::NamedFile;
use rocket::response::{NamedFile, Responder, Response};
use rocket::config::Config;
use rocket_contrib::templates::Template;
use rocket_contrib::templates::{Template, Engines};
use rocket::http::uri::Uri;
use rocket_contrib::json::JsonValue;
use rocket::response::status::Custom;
use rocket_i18n::I18n;
use rocket_prometheus::PrometheusMetrics;
use gettext_macros::{compile_i18n, include_i18n};
use serde::Serialize;
use handlebars::Handlebars;
use std::path::PathBuf;
use mail;
use tokens;
use rate_limiter::RateLimiter;
use crate::mail;
use crate::tokens;
use crate::counters;
use crate::template_helpers::TemplateOverrides;
use crate::i18n::I18NHelper;
use crate::rate_limiter::RateLimiter;
use database::{Database, KeyDatabase, Query};
use database::types::Fingerprint;
use Result;
use crate::database::{Database, KeyDatabase, Query};
use crate::database::types::Fingerprint;
use crate::Result;
use std::convert::TryInto;
@@ -31,16 +39,37 @@ mod vks_web;
mod vks_api;
mod debug_web;
use web::maintenance::MaintenanceMode;
use crate::web::maintenance::MaintenanceMode;
use rocket::http::hyper::header::ContentDisposition;
pub struct HagridTemplate(&'static str, serde_json::Value);
impl Responder<'static> for HagridTemplate {
fn respond_to(self, req: &rocket::Request) -> std::result::Result<Response<'static>, Status> {
let HagridTemplate(tmpl, ctx) = self;
let i18n: I18n = req.guard().expect("Error parsing language");
let template_overrides: rocket::State<TemplateOverrides> = req.guard().expect("TemplateOverrides must be in managed state");
let template_override = template_overrides.get_template_override(i18n.lang, tmpl);
let origin: RequestOrigin = req.guard().expect("Error determining request origin");
let layout_context = templates::HagridLayout::new(ctx, i18n, origin);
if let Some(template_override) = template_override {
Template::render(template_override, layout_context)
} else {
Template::render(tmpl, layout_context)
}.respond_to(req)
}
}
#[derive(Responder)]
pub enum MyResponse {
#[response(status = 200, content_type = "html")]
Success(Template),
Success(HagridTemplate),
#[response(status = 200, content_type = "plain")]
Plain(String),
#[response(status = 200, content_type = "xml")]
Xml(HagridTemplate),
#[response(status = 200, content_type = "application/pgp-keys")]
Key(String, ContentDisposition),
#[response(status = 200, content_type = "application/pgp-keys")]
@@ -48,11 +77,11 @@ pub enum MyResponse {
#[response(status = 500, content_type = "html")]
ServerError(Template),
#[response(status = 404, content_type = "html")]
NotFound(Template),
NotFound(HagridTemplate),
#[response(status = 404, content_type = "html")]
NotFoundPlain(String),
#[response(status = 400, content_type = "html")]
BadRequest(Template),
BadRequest(HagridTemplate),
#[response(status = 400, content_type = "html")]
BadRequestPlain(String),
#[response(status = 503, content_type = "html")]
@@ -64,8 +93,19 @@ pub enum MyResponse {
}
impl MyResponse {
pub fn ok<S: Serialize>(tmpl: &'static str, ctx: S) -> Self {
MyResponse::Success(Template::render(tmpl, ctx))
pub fn ok(tmpl: &'static str, ctx: impl Serialize) -> Self {
let context_json = serde_json::to_value(ctx).unwrap();
MyResponse::Success(HagridTemplate(tmpl, context_json))
}
pub fn ok_bare(tmpl: &'static str) -> Self {
let context_json = serde_json::to_value(templates::Bare { dummy: () }).unwrap();
MyResponse::Success(HagridTemplate(tmpl, context_json))
}
pub fn xml(tmpl: &'static str) -> Self {
let context_json = serde_json::to_value(templates::Bare { dummy: () }).unwrap();
MyResponse::Xml(HagridTemplate(tmpl, context_json))
}
pub fn plain(s: String) -> Self {
@@ -109,17 +149,15 @@ impl MyResponse {
internal_error: e.to_string(),
version: env!("VERGEN_SEMVER").to_string(),
commit: env!("VERGEN_SHA_SHORT").to_string(),
lang: "en".to_string(),
};
MyResponse::ServerError(Template::render("500", ctx))
}
pub fn bad_request(template: &'static str, e: failure::Error) -> Self {
let ctx = templates::General {
error: Some(format!("{}", e)),
version: env!("VERGEN_SEMVER").to_string(),
commit: env!("VERGEN_SHA_SHORT").to_string(),
};
MyResponse::BadRequest(Template::render(template, ctx))
let ctx = templates::Error { error: format!("{}", e) };
let context_json = serde_json::to_value(ctx).unwrap();
MyResponse::BadRequest(HagridTemplate(template, context_json))
}
pub fn bad_request_plain(message: impl Into<String>) -> Self {
@@ -132,64 +170,59 @@ impl MyResponse {
pub fn not_found(
tmpl: Option<&'static str>,
message: impl Into<Option<String>>
message: impl Into<Option<String>>,
) -> Self {
MyResponse::NotFound(
Template::render(
tmpl.unwrap_or("index"),
templates::General::new(
Some(message.into()
.unwrap_or_else(|| "Key not found".to_owned())))))
let ctx = templates::Error { error: message.into()
.unwrap_or_else(|| "Key not found".to_owned()) };
let context_json = serde_json::to_value(ctx).unwrap();
MyResponse::NotFound(HagridTemplate(tmpl.unwrap_or("index"), context_json))
}
}
mod templates {
use super::{I18n, RequestOrigin};
#[derive(Serialize)]
pub struct FiveHundred {
pub internal_error: String,
pub commit: String,
pub version: String,
pub lang: String,
}
#[derive(Serialize)]
pub struct General {
pub struct HagridLayout<T: serde::Serialize> {
pub error: Option<String>,
pub commit: String,
pub version: String,
pub base_uri: String,
pub lang: String,
pub page: T,
}
#[derive(Serialize)]
pub struct About {
pub base_uri: String,
pub commit: String,
pub version: String,
pub struct Error {
pub error: String,
}
impl About {
pub fn new(base_uri: impl Into<String>) -> Self {
#[derive(Serialize)]
pub struct Bare {
// Dummy value to make sure {{#with page}} always passes
pub dummy: (),
}
impl<T: serde::Serialize> HagridLayout<T> {
pub fn new(page: T, i18n: I18n, origin: RequestOrigin) -> Self {
Self {
base_uri: base_uri.into(),
error: None,
version: env!("VERGEN_SEMVER").to_string(),
commit: env!("VERGEN_SHA_SHORT").to_string(),
base_uri: origin.get_base_uri().to_string(),
page: page,
lang: i18n.lang.to_string(),
}
}
}
impl General {
pub fn new(error: Option<String>) -> Self {
Self {
error: error,
version: env!("VERGEN_SEMVER").to_string(),
commit: env!("VERGEN_SHA_SHORT").to_string(),
}
}
}
impl Default for General {
fn default() -> Self {
Self::new(None)
}
}
}
pub struct HagridState {
@@ -271,61 +304,89 @@ fn files(file: PathBuf, state: rocket::State<HagridState>) -> Option<NamedFile>
}
#[get("/")]
fn root() -> Template {
Template::render("index", templates::General::default())
fn root() -> MyResponse {
MyResponse::ok_bare("index")
}
#[get("/about")]
fn about() -> Template {
Template::render("about/about", templates::General::default())
fn about() -> MyResponse {
MyResponse::ok_bare("about/about")
}
#[get("/about/news")]
fn news() -> Template {
Template::render("about/news", templates::General::default())
fn news() -> MyResponse {
MyResponse::ok_bare("about/news")
}
#[get("/atom.xml")]
fn news_atom() -> MyResponse {
MyResponse::xml("atom")
}
#[get("/about/faq")]
fn faq() -> Template {
Template::render("about/faq", templates::General::default())
fn faq() -> MyResponse {
MyResponse::ok_bare("about/faq")
}
#[get("/about/usage")]
fn usage(state: rocket::State<HagridState>) -> Template {
Template::render("about/usage", templates::About::new(state.base_uri.clone()))
fn usage() -> MyResponse {
MyResponse::ok_bare("about/usage")
}
#[get("/about/privacy")]
fn privacy() -> Template {
Template::render("about/privacy", templates::General::default())
fn privacy() -> MyResponse {
MyResponse::ok_bare("about/privacy")
}
#[get("/about/api")]
fn apidoc() -> Template {
Template::render("about/api", templates::General::default())
fn apidoc() -> MyResponse {
MyResponse::ok_bare("about/api")
}
#[get("/about/stats")]
fn stats() -> Template {
Template::render("about/stats", templates::General::default())
fn stats() -> MyResponse {
MyResponse::ok_bare("about/stats")
}
#[get("/errors/<code>/<template>")]
fn errors(
i18n: I18n,
origin: RequestOrigin,
code: u16,
template: String,
) -> Result<Custom<Template>> {
if !template.chars().all(|x| x == '-' || char::is_ascii_alphabetic(&x)) {
return Err(failure::err_msg("bad request"));
}
let status_code = Status::from_code(code)
.ok_or(failure::err_msg("bad request"))?;
let response_body = Template::render(
format!("errors/{}-{}", code, template),
templates::HagridLayout::new(templates::Bare{dummy: ()}, i18n, origin)
);
Ok(Custom(status_code, response_body))
}
pub fn serve() -> Result<()> {
Err(rocket_factory(rocket::ignite())?.launch().into())
}
fn rocket_factory(rocket: rocket::Rocket) -> Result<rocket::Rocket> {
compile_i18n!();
fn rocket_factory(mut rocket: rocket::Rocket) -> Result<rocket::Rocket> {
let routes = routes![
// infra
root,
about,
news,
news_atom,
privacy,
apidoc,
faq,
usage,
files,
stats,
errors,
// VKSv1
vks_api::vks_v1_by_email,
vks_api::vks_v1_by_fingerprint,
@@ -342,6 +403,7 @@ fn rocket_factory(rocket: rocket::Rocket) -> Result<rocket::Rocket> {
vks_web::request_verify_form,
vks_web::request_verify_form_data,
vks_web::verify_confirm,
vks_web::verify_confirm_form,
vks_web::quick_upload,
vks_web::quick_upload_proceed,
// Debug
@@ -350,6 +412,7 @@ fn rocket_factory(rocket: rocket::Rocket) -> Result<rocket::Rocket> {
hkp::pks_lookup,
hkp::pks_add_form,
hkp::pks_add_form_data,
hkp::pks_internal_index,
// EManage
manage::vks_manage,
manage::vks_manage_key,
@@ -368,18 +431,44 @@ fn rocket_factory(rocket: rocket::Rocket) -> Result<rocket::Rocket> {
let mail_service = configure_mail_service(rocket.config())?;
let rate_limiter = configure_rate_limiter(rocket.config())?;
let maintenance_mode = configure_maintenance_mode(rocket.config())?;
let localized_template_list = configure_localized_template_list(rocket.config())?;
println!("{:?}", localized_template_list);
Ok(rocket
.attach(Template::fairing())
let prometheus = configure_prometheus(rocket.config());
rocket = rocket
.attach(Template::custom(|engines: &mut Engines| {
let i18ns = include_i18n!();
let i18n_helper = I18NHelper::new(i18ns);
engines.handlebars.register_helper("text", Box::new(i18n_helper));
}))
.attach(maintenance_mode)
.manage(include_i18n!())
.manage(hagrid_state)
.manage(stateless_token_service)
.manage(stateful_token_service)
.manage(mail_service)
.manage(db_service)
.manage(rate_limiter)
.mount("/", routes)
)
.manage(localized_template_list)
.mount("/", routes);
if let Some(prometheus) = prometheus {
rocket = rocket
.attach(prometheus.clone())
.mount("/metrics", prometheus);
}
Ok(rocket)
}
fn configure_prometheus(config: &Config) -> Option<PrometheusMetrics> {
if !config.get_bool("enable_prometheus").unwrap_or(false) {
return None;
}
let prometheus = PrometheusMetrics::new();
counters::register_counters(&prometheus.registry());
return Some(prometheus);
}
fn configure_db_service(config: &Config) -> Result<KeyDatabase> {
@@ -428,27 +517,18 @@ fn configure_stateless_token_service(config: &Config) -> Result<tokens::Service>
fn configure_mail_service(config: &Config) -> Result<mail::Service> {
// Mail service
let template_dir: PathBuf = config.get_str("template_dir")?.into();
let email_template_dir: PathBuf = config.get_str("email_template_dir")?.into();
let base_uri = config.get_str("base-URI")?.to_string();
let from = config.get_str("from")?.to_string();
let verify_html = template_dir.join("email/publish-html.hbs");
let verify_txt = template_dir.join("email/publish-txt.hbs");
let manage_html = template_dir.join("email/manage-html.hbs");
let manage_txt = template_dir.join("email/manage-txt.hbs");
let mut handlebars = Handlebars::new();
handlebars.register_template_file("verify-html", verify_html)?;
handlebars.register_template_file("verify-txt", verify_txt)?;
handlebars.register_template_file("manage-html", manage_html)?;
handlebars.register_template_file("manage-txt", manage_txt)?;
let filemail_into = config.get_str("filemail_into")
.ok().map(|p| PathBuf::from(p));
if let Some(path) = filemail_into {
mail::Service::filemail(from, base_uri, handlebars, path)
mail::Service::filemail(from, base_uri, email_template_dir, path)
} else {
mail::Service::sendmail(from, base_uri, handlebars)
mail::Service::sendmail(from, base_uri, email_template_dir)
}
}
@@ -458,6 +538,11 @@ fn configure_rate_limiter(config: &Config) -> Result<RateLimiter> {
Ok(RateLimiter::new(timeout_secs))
}
fn configure_localized_template_list(config: &Config) -> Result<TemplateOverrides> {
let template_dir: PathBuf = config.get_str("template_dir")?.into();
TemplateOverrides::load(&template_dir, "localized")
}
fn configure_maintenance_mode(config: &Config) -> Result<MaintenanceMode> {
let maintenance_file: PathBuf = config.get_str("maintenance_file")
.unwrap_or("maintenance").into();
@@ -483,7 +568,7 @@ pub mod tests {
use sequoia_openpgp::parse::Parse;
use sequoia_openpgp::serialize::Serialize;
use database::*;
use crate::database::*;
use super::*;
// for some reason, this is no longer public in lettre itself
@@ -508,7 +593,7 @@ pub mod tests {
/// duration of your test. To debug the test, mem::forget it to
/// prevent cleanup.
pub fn configuration() -> Result<(TempDir, rocket::Config)> {
use rocket::config::{Config, Environment};
use rocket::config::Environment;
let root = tempdir()?;
let filemail = root.path().join("filemail");
@@ -521,6 +606,9 @@ pub mod tests {
.extra("template_dir",
::std::env::current_dir().unwrap().join("dist/templates")
.to_str().unwrap())
.extra("email_template_dir",
::std::env::current_dir().unwrap().join("dist/email-templates")
.to_str().unwrap())
.extra("assets_dir",
::std::env::current_dir().unwrap().join("dist/assets")
.to_str().unwrap())
@@ -553,6 +641,22 @@ pub mod tests {
db.check_consistency().unwrap();
}
#[test]
fn about_translation() {
let (_tmpdir, config) = configuration().unwrap();
let rocket = rocket_factory(rocket::custom(config)).unwrap();
let client = Client::new(rocket).expect("valid rocket instance");
// Check that we see the landing page.
let mut response = client.get("/about")
.header(Header::new("Accept-Language", "de"))
.dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::HTML));
// TODO check translation
assert!(response.body_string().unwrap().contains("Hagrid"));
}
#[test]
fn basics() {
let (_tmpdir, config) = configuration().unwrap();
@@ -593,7 +697,7 @@ pub mod tests {
let mut response = client.get("/manage").dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(), Some(ContentType::HTML));
assert!(response.body_string().unwrap().contains("any verified e-mail address"));
assert!(response.body_string().unwrap().contains("any verified email address"));
assert_consistency(client.rocket());
}
@@ -661,7 +765,7 @@ pub mod tests {
check_hr_responses_by_fingerprint(&client, &tpk, 0);
// Check the verification link
check_verify_link(&client, &token, "foo@invalid.example.com");
check_verify_link(&client, &token, "foo@invalid.example.com", "");
// Now check for the verification mail.
check_mails_and_verify_email(&client, filemail_into.as_path());
@@ -691,6 +795,26 @@ pub mod tests {
assert_consistency(client.rocket());
}
#[test]
fn upload_verify_lang() {
let (tmpdir, client) = client().unwrap();
let filemail_into = tmpdir.path().join("filemail");
// Generate a key and upload it.
let (tpk, _) = TPKBuilder::autocrypt(
None, Some("foo@invalid.example.com"))
.generate().unwrap();
let mut tpk_serialized = Vec::new();
tpk.serialize(&mut tpk_serialized).unwrap();
let token = vks_publish_submit_get_token(&client, &tpk_serialized);
check_verify_link(&client, &token, "foo@invalid.example.com", "de");
let mail_content = pop_mail(&filemail_into).unwrap().unwrap();
assert!(mail_content.contains("Dies ist eine automatisierte Nachricht"));
assert!(mail_content.contains("Subject: =?utf-8?q?Best=C3=A4tige?= foo@invalid.example.com\r\n\t=?utf-8?q?f=C3=BCr?= deinen =?utf-8?q?Schl=C3=BCssel?= auf local.connection"));
}
#[test]
fn upload_two() {
let (_tmpdir, config) = configuration().unwrap();
@@ -765,7 +889,7 @@ pub mod tests {
check_hr_responses_by_fingerprint(&client, &tpk_2, 0);
// Check the verification link
check_verify_link(&client, &token_1, "foo@invalid.example.com");
check_verify_link(&client, &token_1, "foo@invalid.example.com", "");
check_verify_link_json(&client, &token_2, "bar@invalid.example.com");
// Now check for the verification mails.
@@ -860,6 +984,20 @@ pub mod tests {
check_null_responses_by_email(&client, "foo@invalid.example.com");
}
#[test]
fn search_invalid() {
let (_tmpdir, client) = client().unwrap();
check_response(&client, "/search?q=0x1234abcd",
Status::BadRequest, "not supported, sorry!");
check_response(&client, "/search?q=1234abcd",
Status::BadRequest, "not supported, sorry!");
check_response(&client, "/pks/lookup?op=get&search=0x1234abcd",
Status::BadRequest, "not supported, sorry!");
check_response(&client, "/pks/lookup?op=get&search=1234abcd",
Status::BadRequest, "not supported, sorry!");
}
/// Asserts that the given URI 404s.
pub fn check_null_response(client: &Client, uri: &str) {
let response = client.get(uri).dispatch();
@@ -921,6 +1059,25 @@ pub mod tests {
assert_eq!(tpk_.userids().count(), nr_uids);
}
// it's a rather "reverse implementation" style test.. can we do better?
/// Asserts that the given URI returns a correct hkp "index"
/// response for the given TPK.
pub fn check_index_response(client: &Client, uri: &str, tpk: &TPK) {
let mut response = client.get(uri).dispatch();
assert_eq!(response.status(), Status::Ok);
assert_eq!(response.content_type(),
Some(ContentType::new("text", "plain")));
let body = response.body_string().unwrap();
assert!(body.contains("info:1:1"));
let primary_fpr = tpk.fingerprint().to_hex();
let algo: u8 = tpk.primary().pk_algo().into();
assert!(body.contains(&format!("pub:{}:{}:", primary_fpr, algo)));
let creation_time = tpk.primary().creation_time().to_timespec().sec;
assert!(body.contains(&format!(":{}:", creation_time)));
}
/// Asserts that we can get the given TPK back using the various
/// by-fingerprint or by-keyid lookup mechanisms.
pub fn check_mr_responses_by_fingerprint(client: &Client, tpk: &TPK,
@@ -952,6 +1109,20 @@ pub mod tests {
&client,
&format!("/pks/lookup?op=get&search=0x{}", keyid),
&tpk, nr_uids);
check_index_response(
&client,
&format!("/pks/lookup?op=index&search={}", fp),
&tpk);
}
/// Asserts that the given URI contains the search string.
pub fn check_response(client: &Client, uri: &str, status: Status, needle: &str) {
let mut response = client.get(uri).dispatch();
assert_eq!(response.status(), status);
let body = response.body_string().unwrap();
println!("{}", body);
assert!(body.contains(needle));
}
/// Asserts that the given URI returns human readable response
@@ -980,7 +1151,7 @@ pub mod tests {
/// Asserts that the given URI returns human readable response
/// page that contains an onion URI pointing to the TPK.
pub fn check_hr_response_onion(client: &Client, uri: &str, tpk: &TPK,
nr_uids: usize) {
_nr_uids: usize) {
let mut response = client
.get(uri)
.header(Header::new("X-Is-Onion", "true"))
@@ -1022,7 +1193,7 @@ pub mod tests {
&tpk, nr_uids);
}
fn check_verify_link(client: &Client, token: &str, address: &str) {
fn check_verify_link(client: &Client, token: &str, address: &str, lang: &'static str) {
let encoded = ::url::form_urlencoded::Serializer::new(String::new())
.append_pair("token", token)
.append_pair("address", address)
@@ -1030,6 +1201,7 @@ pub mod tests {
let response = client.post("/upload/request-verify")
.header(ContentType::Form)
.header(Header::new("Accept-Language", lang))
.body(encoded.as_bytes())
.dispatch();
assert_eq!(response.status(), Status::Ok);
@@ -1050,8 +1222,12 @@ pub mod tests {
let pattern = format!("{}(/verify/[^ \t\n]*)", BASE_URI);
let confirm_uri = pop_mail_capture_pattern(filemail_path, &pattern);
let response = client.get(&confirm_uri).dispatch();
let response = client.post(&confirm_uri).dispatch();
assert_eq!(response.status(), Status::Ok);
let mut response_second = client.post(&confirm_uri).dispatch();
assert_eq!(response_second.status(), Status::BadRequest);
assert!(response_second.body_string().unwrap().contains("already been verified"));
}
fn check_mails_and_confirm_deletion(client: &Client, filemail_path: &Path, address: &str) {
@@ -1062,7 +1238,6 @@ pub mod tests {
fn pop_mail_capture_pattern(filemail_path: &Path, pattern: &str) -> String {
let mail_content = pop_mail(filemail_path).unwrap().unwrap();
println!("{}", mail_content);
let capture_re = regex::bytes::Regex::new(pattern).unwrap();
let capture_content = capture_re.captures(mail_content.as_ref()).unwrap()
@@ -1079,7 +1254,8 @@ pub mod tests {
let fh = fs::File::open(entry.path())?;
fs::remove_file(entry.path())?;
let mail: SerializableEmail = ::serde_json::from_reader(fh)?;
return Ok(Some(String::from_utf8_lossy(&mail.message).to_string()));
let body = String::from_utf8_lossy(&mail.message).to_string();
return Ok(Some(body));
}
}
Ok(None)

View File

@@ -1,11 +1,15 @@
use failure::Fallible as Result;
use database::{Database, KeyDatabase, StatefulTokens, EmailAddressStatus, TpkStatus};
use database::types::{Fingerprint,Email};
use mail;
use tokens::{self, StatelessSerializable};
use rate_limiter::RateLimiter;
use web::RequestOrigin;
use crate::database::{Database, KeyDatabase, StatefulTokens, EmailAddressStatus, TpkStatus, ImportResult};
use crate::database::types::{Fingerprint,Email};
use crate::mail;
use crate::counters;
use crate::tokens::{self, StatelessSerializable};
use crate::rate_limiter::RateLimiter;
use crate::web::RequestOrigin;
use rocket_i18n::I18n;
use gettext_macros::i18n;
use sequoia_openpgp::TPK;
@@ -29,6 +33,8 @@ pub mod request {
}
pub mod response {
use crate::database::types::Email;
#[derive(Debug,Serialize,Deserialize,PartialEq,Eq)]
pub enum EmailStatus {
#[serde(rename = "unpublished")]
@@ -50,14 +56,16 @@ pub mod response {
is_revoked: bool,
status: HashMap<String,EmailStatus>,
count_unparsed: usize,
is_new_key: bool,
primary_uid: Option<Email>,
},
OkMulti { key_fprs: Vec<String> },
Error(String),
}
impl UploadResponse {
pub fn err(err: &str) -> Self {
UploadResponse::Error(err.to_owned())
pub fn err(err: impl Into<String>) -> Self {
UploadResponse::Error(err.into())
}
}
@@ -67,8 +75,8 @@ pub mod response {
}
impl PublishResponse {
pub fn err(err: &str) -> Self {
PublishResponse::Error(err.to_owned())
pub fn err(err: impl Into<String>) -> Self {
PublishResponse::Error(err.into())
}
}
}
@@ -85,6 +93,7 @@ impl StatelessSerializable for VerifyTpkState {
pub fn process_key(
db: &KeyDatabase,
i18n: &I18n,
tokens_stateless: &tokens::Service,
rate_limiter: &RateLimiter,
reader: impl Read,
@@ -100,28 +109,45 @@ pub fn process_key(
})
{
Ok(ppr) => TPKParser::from_packet_parser(ppr),
Err(_) => return UploadResponse::err("Failed parsing key"),
Err(_) => return UploadResponse::err(i18n!(i18n.catalog, "Parsing of key data failed.")),
};
let mut tpks = Vec::new();
for tpk in parser {
tpks.push(match tpk {
Ok(t) => {
if t.is_tsk() {
return UploadResponse::err("Whoops, please don't upload secret keys!");
counters::inc_key_upload("secret");
return UploadResponse::err(i18n!(
i18n.catalog,
"Whoops, please don't upload secret keys!"
));
}
t
},
Err(_) => return UploadResponse::err("No keys uploaded"),
Err(_) => {
return UploadResponse::err(i18n!(i18n.catalog, "Parsing of key data failed."));
}
});
}
match tpks.len() {
0 => UploadResponse::err("No key submitted"),
1 => process_key_single(db, tokens_stateless, rate_limiter, tpks.into_iter().next().unwrap()),
0 => UploadResponse::err(i18n!(i18n.catalog, "No key uploaded.")),
1 => process_key_single(db, i18n, tokens_stateless, rate_limiter, tpks.into_iter().next().unwrap()),
_ => process_key_multiple(db, tpks),
}
}
fn log_db_merge(import_result: Result<ImportResult>) -> Result<ImportResult> {
match import_result {
Ok(ImportResult::New(_)) => counters::inc_key_upload("new"),
Ok(ImportResult::Updated(_)) => counters::inc_key_upload("updated"),
Ok(ImportResult::Unchanged(_)) => counters::inc_key_upload("unchanged"),
Err(_) => counters::inc_key_upload("error"),
};
import_result
}
fn process_key_multiple(
db: &KeyDatabase,
tpks: Vec<TPK>,
@@ -130,7 +156,7 @@ fn process_key_multiple(
.into_iter()
.flat_map(|tpk| Fingerprint::try_from(tpk.fingerprint())
.map(|fpr| (fpr, tpk)))
.flat_map(|(fpr, tpk)| db.merge(tpk).map(|_| fpr.to_string()))
.flat_map(|(fpr, tpk)| log_db_merge(db.merge(tpk)).map(|_| fpr.to_string()))
.collect();
response::UploadResponse::OkMulti { key_fprs }
@@ -138,16 +164,18 @@ fn process_key_multiple(
fn process_key_single(
db: &KeyDatabase,
i18n: &I18n,
tokens_stateless: &tokens::Service,
rate_limiter: &RateLimiter,
tpk: TPK,
) -> response::UploadResponse {
let fp = Fingerprint::try_from(tpk.fingerprint()).unwrap();
let tpk_status = match db.merge(tpk) {
Ok(import_result) => import_result.into_tpk_status(),
Err(_) => return UploadResponse::err(&format!(
"Something went wrong processing key {}", fp)),
let (tpk_status, is_new_key) = match log_db_merge(db.merge(tpk)) {
Ok(ImportResult::New(tpk_status)) => (tpk_status, true),
Ok(ImportResult::Updated(tpk_status)) => (tpk_status, false),
Ok(ImportResult::Unchanged(tpk_status)) => (tpk_status, false),
Err(_) => return UploadResponse::err(i18n!(i18n.catalog, "Error processing uploaded key.")),
};
let verify_state = {
@@ -163,7 +191,7 @@ fn process_key_single(
let token = tokens_stateless.create(&verify_state);
show_upload_verify(rate_limiter, token, tpk_status, verify_state)
show_upload_verify(rate_limiter, token, tpk_status, verify_state, is_new_key)
}
pub fn request_verify(
@@ -173,17 +201,18 @@ pub fn request_verify(
token_stateless: rocket::State<tokens::Service>,
mail_service: rocket::State<mail::Service>,
rate_limiter: rocket::State<RateLimiter>,
i18n: I18n,
token: String,
addresses: Vec<String>,
) -> response::UploadResponse {
let (verify_state, tpk_status) = match check_tpk_state(&db, &token_stateless, &token) {
let (verify_state, tpk_status) = match check_tpk_state(&db, &token_stateless, &i18n, &token) {
Ok(ok) => ok,
Err(e) => return UploadResponse::err(&e.to_string()),
};
if tpk_status.is_revoked {
return show_upload_verify(
&rate_limiter, token, tpk_status, verify_state);
&rate_limiter, token, tpk_status, verify_state, false);
}
let emails_requested: Vec<_> = addresses.into_iter()
@@ -199,21 +228,26 @@ pub fn request_verify(
for email in emails_requested {
let rate_limit_ok = rate_limiter.action_perform(format!("verify-{}", &email));
if rate_limit_ok {
if send_verify_email(&request_origin, &mail_service, &token_stateful, &verify_state.fpr, &email).is_err() {
if send_verify_email(&request_origin, &mail_service, &token_stateful, &i18n, &verify_state.fpr, &email).is_err() {
return UploadResponse::err(&format!("error sending email to {}", &email));
}
}
}
show_upload_verify(&rate_limiter, token, tpk_status, verify_state)
show_upload_verify(&rate_limiter, token, tpk_status, verify_state, false)
}
fn check_tpk_state(
db: &KeyDatabase,
token_stateless: &tokens::Service,
i18n: &I18n,
token: &str,
) -> Result<(VerifyTpkState,TpkStatus)> {
let verify_state = token_stateless.check::<VerifyTpkState>(token)?;
let verify_state = token_stateless.check::<VerifyTpkState>(token)
.map_err(|_| failure::err_msg(i18n!(
i18n.catalog,
"Upload session expired. Please try again."
)))?;
let tpk_status = db.get_tpk_status(&verify_state.fpr, &verify_state.addresses)?;
Ok((verify_state, tpk_status))
}
@@ -222,6 +256,7 @@ fn send_verify_email(
request_origin: &RequestOrigin,
mail_service: &mail::Service,
token_stateful: &StatefulTokens,
i18n: &I18n,
fpr: &Fingerprint,
email: &Email,
) -> Result<()> {
@@ -230,6 +265,7 @@ fn send_verify_email(
let token_verify = token_stateful.new_token("verify", token_str.as_bytes())?;
mail_service.send_verification(
i18n,
request_origin.get_base_uri(),
fpr.to_string(),
&email,
@@ -239,12 +275,14 @@ fn send_verify_email(
pub fn verify_confirm(
db: rocket::State<KeyDatabase>,
i18n: &I18n,
token_service: rocket::State<StatefulTokens>,
token: String,
) -> response::PublishResponse {
let (fingerprint, email) = match check_publish_token(&db, &token_service, token) {
Ok(x) => x,
Err(_) => return PublishResponse::err("token verification failed"),
Err(_) => return PublishResponse::err(
i18n!(i18n.catalog, "Invalid verification link.")),
};
response::PublishResponse::Ok {
@@ -260,7 +298,9 @@ fn check_publish_token(
) -> Result<(Fingerprint,Email)> {
let payload = token_service.pop_token("verify", &token)?;
let (fingerprint, email) = serde_json::from_str(&payload)?;
db.set_email_published(&fingerprint, &email)?;
counters::inc_address_published(&email);
Ok((fingerprint, email))
}
@@ -270,10 +310,19 @@ fn show_upload_verify(
token: String,
tpk_status: TpkStatus,
verify_state: VerifyTpkState,
is_new_key: bool,
) -> response::UploadResponse {
let key_fpr = verify_state.fpr.to_string();
if tpk_status.is_revoked {
return response::UploadResponse::Ok { token, key_fpr, count_unparsed: 0, is_revoked: true, status: HashMap::new() };
return response::UploadResponse::Ok {
token,
key_fpr,
count_unparsed: 0,
is_revoked: true,
status: HashMap::new(),
is_new_key: false,
primary_uid: None,
};
}
let status: HashMap<_,_> = tpk_status.email_status
@@ -292,8 +341,12 @@ fn show_upload_verify(
}
})
.collect();
let primary_uid = tpk_status.email_status
.get(0)
.map(|(email, _)| email)
.cloned();
let count_unparsed = tpk_status.unparsed_uids;
response::UploadResponse::Ok { token, key_fpr, count_unparsed, is_revoked: false, status }
response::UploadResponse::Ok { token, key_fpr, count_unparsed, is_revoked: false, status, is_new_key, primary_uid }
}

View File

@@ -2,27 +2,30 @@ use rocket_contrib::json::{Json,JsonValue,JsonError};
use rocket::request::Request;
use rocket::response::{self, Response, Responder};
use rocket::http::{ContentType,Status};
use rocket::State;
use rocket_i18n::{I18n, Translations};
use std::io::Cursor;
use database::{KeyDatabase, StatefulTokens, Query};
use database::types::{Email, Fingerprint, KeyID};
use mail;
use tokens;
use rate_limiter::RateLimiter;
use crate::database::{KeyDatabase, StatefulTokens, Query};
use crate::database::types::{Email, Fingerprint, KeyID};
use crate::mail;
use crate::tokens;
use crate::rate_limiter::RateLimiter;
use web;
use web::{HagridState, RequestOrigin, MyResponse};
use web::vks;
use web::vks::response::*;
use crate::web;
use crate::web::{HagridState, RequestOrigin, MyResponse};
use crate::web::vks;
use crate::web::vks::response::*;
pub mod json {
use web::vks::response::EmailStatus;
use crate::web::vks::response::EmailStatus;
use std::collections::HashMap;
#[derive(Deserialize)]
pub struct VerifyRequest {
pub token: String,
pub addresses: Vec<String>,
pub locale: Option<Vec<String>>,
}
#[derive(Deserialize)]
@@ -76,12 +79,13 @@ pub fn upload_json(
db: rocket::State<KeyDatabase>,
tokens_stateless: rocket::State<tokens::Service>,
rate_limiter: rocket::State<RateLimiter>,
i18n: I18n,
data: Result<Json<json::UploadRequest>, JsonError>,
) -> JsonResult {
let data = json_or_error(data)?;
use std::io::Cursor;
let data_reader = Cursor::new(data.keytext.as_bytes());
let result = vks::process_key(&db, &tokens_stateless, &rate_limiter, data_reader);
let result = vks::process_key(&db, &i18n, &tokens_stateless, &rate_limiter, data_reader);
upload_ok_json(result)
}
@@ -93,9 +97,24 @@ pub fn upload_fallback(
JsonErrorResponse(Status::BadRequest, error_msg)
}
fn get_locale(
langs: State<Translations>,
locales: Vec<String>,
) -> I18n {
locales
.iter()
.flat_map(|lang| lang.split(|c| c == '-' || c == ';' || c == '_').next())
.flat_map(|lang| langs.iter().find(|(trans, _)| trans == &lang))
.next()
.or_else(|| langs.iter().find(|(trans, _)| trans == &"en"))
.map(|(lang, catalog)| I18n { catalog: catalog.clone(), lang })
.expect("Expected to have an english translation!")
}
#[post("/vks/v1/request-verify", format = "json", data="<data>")]
pub fn request_verify_json(
db: rocket::State<KeyDatabase>,
langs: State<Translations>,
request_origin: RequestOrigin,
token_stateful: rocket::State<StatefulTokens>,
token_stateless: rocket::State<tokens::Service>,
@@ -104,10 +123,11 @@ pub fn request_verify_json(
data: Result<Json<json::VerifyRequest>, JsonError>,
) -> JsonResult {
let data = json_or_error(data)?;
let json::VerifyRequest { token, addresses } = data.into_inner();
let json::VerifyRequest { token, addresses, locale } = data.into_inner();
let i18n = get_locale(langs, locale.unwrap_or_default());
let result = vks::request_verify(
db, request_origin, token_stateful, token_stateless, mail_service,
rate_limiter, token, addresses);
rate_limiter, i18n, token, addresses);
upload_ok_json(result)
}
@@ -125,7 +145,7 @@ pub fn vks_v1_by_fingerprint(state: rocket::State<HagridState>,
fpr: String) -> MyResponse {
let query = match fpr.parse::<Fingerprint>() {
Ok(fpr) => Query::ByFingerprint(fpr),
Err(e) => return MyResponse::bad_request("index", e),
Err(_) => return MyResponse::bad_request_plain("malformed fingerprint"),
};
web::key_to_response_plain(state, db, query)
@@ -135,9 +155,10 @@ pub fn vks_v1_by_fingerprint(state: rocket::State<HagridState>,
pub fn vks_v1_by_email(state: rocket::State<HagridState>,
db: rocket::State<KeyDatabase>,
email: String) -> MyResponse {
let email = email.replace("%40", "@");
let query = match email.parse::<Email>() {
Ok(email) => Query::ByEmail(email),
Err(e) => return MyResponse::bad_request("index", e),
Err(_) => return MyResponse::bad_request_plain("malformed e-mail address"),
};
web::key_to_response_plain(state, db, query)
@@ -149,7 +170,7 @@ pub fn vks_v1_by_keyid(state: rocket::State<HagridState>,
kid: String) -> MyResponse {
let query = match kid.parse::<KeyID>() {
Ok(keyid) => Query::ByKeyID(keyid),
Err(e) => return MyResponse::bad_request("index", e),
Err(_) => return MyResponse::bad_request_plain("malformed key id"),
};
web::key_to_response_plain(state, db, query)

View File

@@ -8,18 +8,20 @@ use multipart::server::Multipart;
use rocket::http::ContentType;
use rocket::request::Form;
use rocket::Data;
use rocket_i18n::I18n;
use gettext_macros::i18n;
use database::{KeyDatabase, StatefulTokens, Query, Database};
use mail;
use tokens;
use web::{RequestOrigin, MyResponse};
use rate_limiter::RateLimiter;
use crate::database::{KeyDatabase, StatefulTokens, Query, Database};
use crate::mail;
use crate::tokens;
use crate::web::{RequestOrigin, MyResponse};
use crate::rate_limiter::RateLimiter;
use std::io::Read;
use std::collections::HashMap;
use web::vks;
use web::vks::response::*;
use crate::web::vks;
use crate::web::vks::response::*;
const UPLOAD_LIMIT: u64 = 1024 * 1024; // 1 MiB.
@@ -37,35 +39,26 @@ mod forms {
}
mod template {
#[derive(Serialize)]
pub struct VerifyForm {
pub token: String,
}
#[derive(Serialize)]
pub struct Verify {
pub verified: bool,
pub key_fpr: String,
pub userid: String,
pub userid_link: String,
pub commit: String,
pub version: String,
}
#[derive(Serialize)]
pub struct Search {
pub query: String,
pub fpr: String,
pub base_uri: String,
pub commit: String,
pub version: String,
}
#[derive(Serialize)]
pub struct Upload {
pub commit: String,
pub version: String,
}
#[derive(Serialize)]
pub struct VerificationSent {
pub commit: String,
pub version: String,
pub key_fpr: String,
pub key_link: String,
pub is_revoked: bool,
@@ -86,8 +79,6 @@ mod template {
#[derive(Serialize)]
pub struct UploadOkMultiple {
pub commit: String,
pub version: String,
pub keys: Vec<UploadOkKey>,
}
@@ -105,7 +96,7 @@ impl MyResponse {
UploadResponse::Ok { token, .. } => {
let uri = uri!(quick_upload_proceed: token);
let text = format!(
"Key successfully uploaded. Proceed here:\n{}{}\n",
"Key successfully uploaded. Proceed with verification here:\n{}{}\n",
base_uri,
uri
);
@@ -120,7 +111,7 @@ impl MyResponse {
fn upload_response(response: UploadResponse) -> Self {
match response {
UploadResponse::Ok { token, key_fpr, is_revoked, count_unparsed, status } =>
UploadResponse::Ok { token, key_fpr, is_revoked, count_unparsed, status, .. } =>
Self::upload_ok(token, key_fpr, is_revoked, count_unparsed, status),
UploadResponse::OkMulti { key_fprs } =>
Self::upload_ok_multi(key_fprs),
@@ -161,8 +152,6 @@ impl MyResponse {
.sort_unstable_by(|fst,snd| fst.address.cmp(&snd.address));
let context = template::VerificationSent {
version: env!("VERGEN_SEMVER").to_string(),
commit: env!("VERGEN_SHA_SHORT").to_string(),
is_revoked,
key_fpr,
key_link,
@@ -189,8 +178,6 @@ impl MyResponse {
.collect();
let context = template::UploadOkMultiple {
version: env!("VERGEN_SEMVER").to_string(),
commit: env!("VERGEN_SHA_SHORT").to_string(),
keys,
};
@@ -200,12 +187,7 @@ impl MyResponse {
#[get("/upload")]
pub fn upload() -> MyResponse {
let context = template::Upload {
version: env!("VERGEN_SEMVER").to_string(),
commit: env!("VERGEN_SHA_SHORT").to_string(),
};
MyResponse::ok("upload/upload", context)
MyResponse::ok_bare("upload/upload")
}
#[post("/upload/submit", format = "multipart/form-data", data = "<data>")]
@@ -213,36 +195,46 @@ pub fn upload_post_form_data(
db: rocket::State<KeyDatabase>,
tokens_stateless: rocket::State<tokens::Service>,
rate_limiter: rocket::State<RateLimiter>,
i18n: I18n,
cont_type: &ContentType,
data: Data,
) -> Result<MyResponse> {
// multipart/form-data
let (_, boundary) =
match cont_type.params().find(|&(k, _)| k == "boundary") {
Some(v) => v,
None => return Ok(MyResponse::bad_request(
"upload/upload",
failure::err_msg("`Content-Type: multipart/form-data` \
boundary param not provided"))),
};
) -> MyResponse {
match process_post_form_data(db, tokens_stateless, rate_limiter, i18n, cont_type, data) {
Ok(response) => MyResponse::upload_response(response),
Err(err) => MyResponse::bad_request("upload/upload", err),
}
}
process_upload(&db, &tokens_stateless, &rate_limiter, data, boundary)
pub fn process_post_form_data(
db: rocket::State<KeyDatabase>,
tokens_stateless: rocket::State<tokens::Service>,
rate_limiter: rocket::State<RateLimiter>,
i18n: I18n,
cont_type: &ContentType,
data: Data,
) -> Result<UploadResponse> {
// multipart/form-data
let (_, boundary) = cont_type
.params()
.find(|&(k, _)| k == "boundary")
.ok_or_else(|| failure::err_msg("`Content-Type: multipart/form-data` \
boundary param not provided"))?;
process_upload(&db, &tokens_stateless, &rate_limiter, &i18n, data, boundary)
}
#[get("/search?<q>")]
pub fn search(
request_origin: RequestOrigin,
db: rocket::State<KeyDatabase>,
q: String,
) -> MyResponse {
match q.parse::<Query>() {
Ok(query) => key_to_response(request_origin, db, q, query),
Ok(query) => key_to_response(db, q, query),
Err(e) => MyResponse::bad_request("index", e),
}
}
fn key_to_response(
request_origin: RequestOrigin,
db: rocket::State<KeyDatabase>,
query_string: String,
query: Query,
@@ -255,10 +247,7 @@ fn key_to_response(
let context = template::Search{
query: query_string,
base_uri: request_origin.get_base_uri().to_owned(),
fpr: fp.to_string(),
version: env!("VERGEN_SEMVER").to_string(),
commit: env!("VERGEN_SHA_SHORT").to_string(),
};
MyResponse::ok("found", context)
@@ -270,6 +259,7 @@ pub fn quick_upload(
db: rocket::State<KeyDatabase>,
tokens_stateless: rocket::State<tokens::Service>,
rate_limiter: rocket::State<RateLimiter>,
i18n: I18n,
request_origin: RequestOrigin,
data: Data,
) -> MyResponse {
@@ -280,11 +270,14 @@ pub fn quick_upload(
return MyResponse::bad_request("400-plain", failure::err_msg(error));
}
MyResponse::upload_response_quick(vks::process_key(
MyResponse::upload_response_quick(
vks::process_key(
&db,
&i18n,
&tokens_stateless,
&rate_limiter,
Cursor::new(buf)), request_origin.get_base_uri())
Cursor::new(buf)
), request_origin.get_base_uri())
}
#[get("/upload/<token>", rank = 2)]
@@ -295,11 +288,12 @@ pub fn quick_upload_proceed(
token_stateless: rocket::State<tokens::Service>,
mail_service: rocket::State<mail::Service>,
rate_limiter: rocket::State<RateLimiter>,
i18n: I18n,
token: String,
) -> MyResponse {
let result = vks::request_verify(
db, request_origin, token_stateful, token_stateless, mail_service,
rate_limiter, token, vec!());
rate_limiter, i18n, token, vec!());
MyResponse::upload_response(result)
}
@@ -309,8 +303,22 @@ pub fn upload_post_form(
db: rocket::State<KeyDatabase>,
tokens_stateless: rocket::State<tokens::Service>,
rate_limiter: rocket::State<RateLimiter>,
i18n: I18n,
data: Data,
) -> Result<MyResponse> {
) -> MyResponse {
match process_post_form(&db, &tokens_stateless, &rate_limiter, &i18n, data) {
Ok(response) => MyResponse::upload_response(response),
Err(err) => MyResponse::bad_request("upload/upload", err),
}
}
pub fn process_post_form(
db: &KeyDatabase,
tokens_stateless: &tokens::Service,
rate_limiter: &RateLimiter,
i18n: &I18n,
data: Data,
) -> Result<UploadResponse> {
use rocket::request::FormItems;
use std::io::Cursor;
@@ -329,19 +337,19 @@ pub fn upload_post_form(
match key.as_str() {
"keytext" => {
return Ok(MyResponse::upload_response(vks::process_key(
return Ok(vks::process_key(
&db,
&i18n,
&tokens_stateless,
&rate_limiter,
Cursor::new(decoded_value.as_bytes())
)));
));
}
_ => { /* skip */ }
}
}
Ok(MyResponse::bad_request("upload/upload",
failure::err_msg("No keytext found")))
Err(failure::err_msg("No keytext found"))
}
@@ -349,18 +357,19 @@ fn process_upload(
db: &KeyDatabase,
tokens_stateless: &tokens::Service,
rate_limiter: &RateLimiter,
i18n: &I18n,
data: Data,
boundary: &str,
) -> Result<MyResponse> {
) -> Result<UploadResponse> {
// saves all fields, any field longer than 10kB goes to a temporary directory
// Entries could implement FromData though that would give zero control over
// how the files are saved; Multipart would be a good impl candidate though
match Multipart::with_body(data.open().take(UPLOAD_LIMIT), boundary).save().temp() {
Full(entries) => {
process_multipart(db, tokens_stateless, rate_limiter, entries)
process_multipart(db, tokens_stateless, rate_limiter, i18n, entries)
}
Partial(partial, _) => {
process_multipart(db, tokens_stateless, rate_limiter, partial.entries)
process_multipart(db, tokens_stateless, rate_limiter, i18n, partial.entries)
}
Error(err) => Err(err.into())
}
@@ -370,19 +379,16 @@ fn process_multipart(
db: &KeyDatabase,
tokens_stateless: &tokens::Service,
rate_limiter: &RateLimiter,
i18n: &I18n,
entries: Entries,
) -> Result<MyResponse> {
) -> Result<UploadResponse> {
match entries.fields.get("keytext") {
Some(ent) if ent.len() == 1 => {
let reader = ent[0].data.readable()?;
Ok(MyResponse::upload_response(vks::process_key(db, tokens_stateless, rate_limiter, reader)))
Ok(vks::process_key(db, i18n, tokens_stateless, rate_limiter, reader))
}
Some(_) =>
Ok(MyResponse::bad_request(
"upload/upload", failure::err_msg("Multiple keytexts found"))),
None =>
Ok(MyResponse::bad_request(
"upload/upload", failure::err_msg("No keytext found"))),
Some(_) => Err(failure::err_msg("Multiple keytexts found")),
None => Err(failure::err_msg("No keytext found")),
}
}
@@ -394,12 +400,13 @@ pub fn request_verify_form(
token_stateless: rocket::State<tokens::Service>,
mail_service: rocket::State<mail::Service>,
rate_limiter: rocket::State<RateLimiter>,
i18n: I18n,
request: Form<forms::VerifyRequest>,
) -> MyResponse {
let forms::VerifyRequest { token, address } = request.into_inner();
let result = vks::request_verify(
db, request_origin, token_stateful, token_stateless, mail_service,
rate_limiter, token, vec!(address));
rate_limiter, i18n, token, vec!(address));
MyResponse::upload_response(result)
}
@@ -411,36 +418,53 @@ pub fn request_verify_form_data(
token_stateless: rocket::State<tokens::Service>,
mail_service: rocket::State<mail::Service>,
rate_limiter: rocket::State<RateLimiter>,
i18n: I18n,
request: Form<forms::VerifyRequest>,
) -> MyResponse {
let forms::VerifyRequest { token, address } = request.into_inner();
let result = vks::request_verify(
db, request_origin, token_stateful, token_stateless, mail_service,
rate_limiter, token, vec!(address));
rate_limiter, i18n, token, vec!(address));
MyResponse::upload_response(result)
}
#[get("/verify/<token>")]
#[post("/verify/<token>")]
pub fn verify_confirm(
db: rocket::State<KeyDatabase>,
token_service: rocket::State<StatefulTokens>,
rate_limiter: rocket::State<RateLimiter>,
i18n: I18n,
token: String,
) -> MyResponse {
match vks::verify_confirm(db, token_service, token) {
let rate_limit_id = format!("verify-token-{}", &token);
match vks::verify_confirm(db, &i18n, token_service, token) {
PublishResponse::Ok { fingerprint, email } => {
rate_limiter.action_perform(rate_limit_id);
let userid_link = uri!(search: &email).to_string();
let context = template::Verify {
verified: true,
userid: email,
key_fpr: fingerprint,
userid_link,
version: env!("VERGEN_SEMVER").to_string(),
commit: env!("VERGEN_SHA_SHORT").to_string(),
};
MyResponse::ok("upload/publish-result", context)
},
PublishResponse::Error(error) => MyResponse::plain(error),
PublishResponse::Error(error) => {
let error_msg = if rate_limiter.action_check(rate_limit_id) {
failure::err_msg(error)
} else {
failure::err_msg(i18n!(i18n.catalog, "This address has already been verified."))
};
MyResponse::bad_request("400", error_msg)
}
}
}
#[get("/verify/<token>")]
pub fn verify_confirm_form(
token: String,
) -> MyResponse {
MyResponse::ok("upload/verification-form", template::VerifyForm {
token
})
}

View File

@@ -0,0 +1,29 @@
<div class="about">
<center><h2>Übersicht | <a href="/about/news">News</a> | <a href="/about/usage">Nutzung</a> | <a href="/about/faq">FAQ</a> | <a href="/about/stats">Statistik</a> | <a href="/about/privacy">Privacy Policy</a>
</h2></center>
<p>Der <tt>keys.openpgp.org</tt> Server ist ein öffentlicher Service für die Verteilung von OpenPGP-Schlüsseln, üblicherweise als "Keyserver" bezeichnet.</p>
<p><strong>Für Details zur Nutzung, siehe <a href="/about/usage">Nutzungshinweise</a></strong></p>
<h3>Funktionsweise</h3>
<p>Ein OpenPGP Schlüssel enthält zwei Arten Information:</p>
<ul>
<li>
<strong>Identitäten</strong> beschreiben die Teile des Schlüssels, welche den Besitzer identifizieren. Sie sind auch bekannt als "User IDs", und enhalten typischerweise einen Namen und eine E-Mail-Adresse.</li>
<li>Die <strong>Nicht-Identitäts-Informationen</strong> enthalten techische Details über den Schlüssel an sich. Dies sind insbesondere die mathematischen Objekte, mit deren Hilfe Signaturen berechnet und Nachrichten verschlüsselt werden. Auch enthält dies Metadaten des Schlüssels wie den Zeitpunkt der Erstellung, ein eventuelles Ablaufdatum, und den Widerrufsstatus.</li>
</ul>
<p>Traditionell wurden diese Bestandteile von Keyservern gemeinsam ausgeliefert. Auf <span class="brand">keys.openpgp.org</span> werden sie jedoch getrennt behandelt: Während Nicht-Identität-Informationen jedes Schlüssels frei hochgeladen werden können, werden Identitäts-Informationen nur unter bestimmten Bedingungen gespeichert und weiter veröffentlicht:</p>
<p>Die <strong>Nicht-Identitäts-Informationen</strong> eines Schlüssels sind rein technischer Natur, und können nicht zur direkten Identifikation von Personen verwendet werden. Sie werden nach Prüfung der kryptografischen Integrität ohne weitere Bestätigung gespeichert und verteilt. Fortgeschrittene OpenPGP-Anwendungen können <span class="brand">keys.openpgp.org</span> verwenden, um diese Informationen in allen bekannten Schlüsseln auf dem neusten Stand zu halten, und eine sichere und zuverlässige Kommunikation sicherzustellen.</p>
<p>Die <strong>Identitäts-Informationen </strong> eines OpenPGP-Schlüssels enthalten persönliche Daten ihres Besitzers. Sie werden ausschließlich mit dessen Zustimmung veröffentlicht, welche mit einer simplen Bestätigung via E-Mail abgefragt wird. Insbesondere kann ein Schlüssel nur mit Zustimmung anhand seiner E-Mail-Adressen in der Suche gefunden werden.</p>
<h3 id="community">Community und Plattform</h3>
<p>Dieser Dienst wird ehrenamtlich unterhalten. Die Betreiber dieses Dienstes kommen aus unterschiedlichen OpenPGP-Projekten, insbesondere Sequoia-PGP, OpenKeychain, und Enigmail. Du kannst uns unter #hagrid im Freenode IRC erreichen, oder #hagrid:stratum0.org auf Matrix. Natürlich sind wir auch per E-Mail verfügbar, unter <tt>support at keys punkt openpgp punkt org</tt>.</p>
<p>Technisch verwendet <tt>keys.openpgp.org</tt> die <a href="https://gitlab.com/hagrid-keyserver/hagrid" target="_blank">Hagrid</a> Keyserver-Software, welche auf <a href="https://sequoia-pgp.org">Sequoia-PGP</a> basiert. Der Dienst wird gehostet auf der <a href="https://eclips.is" target="_blank">eclips.is</a> Plattform, die von <a href="https://greenhost.net/" target="_blank">Greenhost</a> unterhalten wird und sich auf Internet-Freedom-Projekte spezialisiert.</p>
</div>

Some files were not shown because too many files have changed in this diff Show More