mailcycle 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/lib/mailcycle/client.rb +9 -10
- data/lib/mailcycle/crypto/identity.rb +2 -2
- data/lib/mailcycle/crypto/mnemonic.rb +3 -10
- data/lib/mailcycle/keys.rb +2 -2
- data/lib/mailcycle/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 00ac2000980d75a7daff3d987843ec8a7f55f8901972343322a96c73180834b4
|
|
4
|
+
data.tar.gz: c1f1d87eca6363dbd6d304f7fceef45ddc82ba6cb0ae0dadab8498dc84c1b8d6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d9794b89fd4d79c0b6db76614fc3483862a933f792b198d5b748cd7fe203515f4c6c1170d07a71480db73102cedf42632947ff8ba6cbaab03e525696ee6ecd0a
|
|
7
|
+
data.tar.gz: b29ab90068d15725085ab6eb2034d53eb59ec500acd5647e93f4f5636125ba17f288fbfa1a5fb1e7d7c4e2473e141849923d1329695b8ee1d85d85550abf7dd9
|
data/README.md
CHANGED
|
@@ -205,4 +205,4 @@ The encryption is checked against Mailcycle's published test vectors (https://do
|
|
|
205
205
|
|
|
206
206
|
## Licence
|
|
207
207
|
|
|
208
|
-
Apache License 2.0. Copyright (c) 2026 Northlab Studios Ltd. You may use, modify and redistribute this gem, including inside your own applications, under the terms in
|
|
208
|
+
Apache License 2.0. Copyright (c) 2026 Northlab Studios Ltd. You may use, modify and redistribute this gem, including inside your own applications, under the terms in the LICENSE file included with it. Using the Mailcycle API itself is governed by the [Terms of Service](https://mailcycle.email/legal/terms/). Never ship an API key in software you distribute.
|
data/lib/mailcycle/client.rb
CHANGED
|
@@ -81,8 +81,7 @@ module Mailcycle
|
|
|
81
81
|
# that this machine holds the matching private one.
|
|
82
82
|
#
|
|
83
83
|
# Every constructor takes the same options: `api_url` (the live API by
|
|
84
|
-
# default)
|
|
85
|
-
# `timeout` in seconds (30).
|
|
84
|
+
# default) and `timeout` in seconds (30).
|
|
86
85
|
class Client
|
|
87
86
|
attr_reader :http, :keys
|
|
88
87
|
|
|
@@ -96,8 +95,8 @@ module Mailcycle
|
|
|
96
95
|
# The same challenge-response the app uses, in which nothing secret
|
|
97
96
|
# crosses the wire. Sessions last 30 days; keep `session_token` and pass
|
|
98
97
|
# it to `resume` rather than deriving the keys again every run.
|
|
99
|
-
def self.sign_in(phrase, api_url: Http::DEFAULT_API_URL,
|
|
100
|
-
keys = Keys.from_phrase(phrase
|
|
98
|
+
def self.sign_in(phrase, api_url: Http::DEFAULT_API_URL, timeout: Http::DEFAULT_TIMEOUT_SECONDS)
|
|
99
|
+
keys = Keys.from_phrase(phrase)
|
|
101
100
|
http = Http.new(api_url, nil, timeout)
|
|
102
101
|
|
|
103
102
|
challenge = Wire.object(http.request("POST", "/accounts/challenge", body: { "accountId" => keys.account_id }))
|
|
@@ -116,9 +115,9 @@ module Mailcycle
|
|
|
116
115
|
#
|
|
117
116
|
# The phrase is still needed to open mail, and is checked against the
|
|
118
117
|
# session's account before it is used.
|
|
119
|
-
def self.resume(session_token, phrase, api_url: Http::DEFAULT_API_URL,
|
|
118
|
+
def self.resume(session_token, phrase, api_url: Http::DEFAULT_API_URL,
|
|
120
119
|
timeout: Http::DEFAULT_TIMEOUT_SECONDS)
|
|
121
|
-
keys = Keys.from_phrase(phrase
|
|
120
|
+
keys = Keys.from_phrase(phrase)
|
|
122
121
|
client = new(Http.new(api_url, session_token, timeout), keys, api_key: false)
|
|
123
122
|
client.__send__(:check_phrase_matches, "session")
|
|
124
123
|
client
|
|
@@ -128,9 +127,9 @@ module Mailcycle
|
|
|
128
127
|
#
|
|
129
128
|
# With the phrase as well, the client can open mail and create addresses
|
|
130
129
|
# that receive it; the phrase is checked against the key's account first.
|
|
131
|
-
def self.with_api_key(api_key, phrase: nil, api_url: Http::DEFAULT_API_URL,
|
|
130
|
+
def self.with_api_key(api_key, phrase: nil, api_url: Http::DEFAULT_API_URL,
|
|
132
131
|
timeout: Http::DEFAULT_TIMEOUT_SECONDS)
|
|
133
|
-
keys = phrase.nil? ? nil : Keys.from_phrase(phrase
|
|
132
|
+
keys = phrase.nil? ? nil : Keys.from_phrase(phrase)
|
|
134
133
|
client = new(Http.new(api_url, api_key, timeout), keys, api_key: true)
|
|
135
134
|
client.__send__(:check_phrase_matches, "API key") if keys
|
|
136
135
|
client
|
|
@@ -140,10 +139,10 @@ module Mailcycle
|
|
|
140
139
|
#
|
|
141
140
|
# Keep the phrase. Nothing can recover the account without it, and nobody
|
|
142
141
|
# at Mailcycle can help.
|
|
143
|
-
def self.create_account(phrase = nil, api_url: Http::DEFAULT_API_URL,
|
|
142
|
+
def self.create_account(phrase = nil, api_url: Http::DEFAULT_API_URL,
|
|
144
143
|
timeout: Http::DEFAULT_TIMEOUT_SECONDS)
|
|
145
144
|
words = phrase || generate_phrase
|
|
146
|
-
keys = Keys.from_phrase(words
|
|
145
|
+
keys = Keys.from_phrase(words)
|
|
147
146
|
http = Http.new(api_url, nil, timeout)
|
|
148
147
|
session = Wire.object(http.request("POST", "/accounts", body: {
|
|
149
148
|
"accountId" => keys.account_id,
|
|
@@ -90,8 +90,8 @@ module Mailcycle
|
|
|
90
90
|
)
|
|
91
91
|
end
|
|
92
92
|
|
|
93
|
-
def identity_from_phrase(phrase
|
|
94
|
-
identity_from_seed(Mnemonic.phrase_to_seed(phrase
|
|
93
|
+
def identity_from_phrase(phrase)
|
|
94
|
+
identity_from_seed(Mnemonic.phrase_to_seed(phrase))
|
|
95
95
|
end
|
|
96
96
|
|
|
97
97
|
# The verifier the server is allowed to store.
|
|
@@ -19,9 +19,7 @@ module Mailcycle
|
|
|
19
19
|
# Derivation cost.
|
|
20
20
|
#
|
|
21
21
|
# Changing it changes every account id ever derived, so this number is
|
|
22
|
-
# part of the format, not a tuning knob.
|
|
23
|
-
# 128-bit phrase and earns its keep only for the optional passphrase,
|
|
24
|
-
# which may be low entropy.
|
|
22
|
+
# part of the format, not a tuning knob.
|
|
25
23
|
SEED_ITERATIONS = 120_000
|
|
26
24
|
SEED_BYTES = 64
|
|
27
25
|
SEED_SALT_PREFIX = "mailcycle-seed"
|
|
@@ -112,14 +110,9 @@ module Mailcycle
|
|
|
112
110
|
end
|
|
113
111
|
|
|
114
112
|
# Phrase to 64-byte root seed.
|
|
115
|
-
|
|
116
|
-
# The optional passphrase is BIP-39's "25th word": it changes which
|
|
117
|
-
# account the phrase opens, and nothing anywhere records whether one was
|
|
118
|
-
# used.
|
|
119
|
-
def phrase_to_seed(phrase, passphrase = "")
|
|
113
|
+
def phrase_to_seed(phrase)
|
|
120
114
|
words = normalize(phrase).join(" ")
|
|
121
|
-
|
|
122
|
-
Primitives.pbkdf2_sha256(words, salt, SEED_ITERATIONS, SEED_BYTES)
|
|
115
|
+
Primitives.pbkdf2_sha256(words, SEED_SALT_PREFIX.b, SEED_ITERATIONS, SEED_BYTES)
|
|
123
116
|
end
|
|
124
117
|
end
|
|
125
118
|
end
|
data/lib/mailcycle/keys.rb
CHANGED
|
@@ -9,11 +9,11 @@ module Mailcycle
|
|
|
9
9
|
# the iteration count is the format either way, and changing it would change
|
|
10
10
|
# every account id ever derived.
|
|
11
11
|
class Keys
|
|
12
|
-
def self.from_phrase(phrase
|
|
12
|
+
def self.from_phrase(phrase)
|
|
13
13
|
problem = Crypto::Mnemonic.validate(phrase)
|
|
14
14
|
raise PhraseError, problem unless problem.nil?
|
|
15
15
|
|
|
16
|
-
new(Crypto::Identity.identity_from_seed(Crypto::Mnemonic.phrase_to_seed(phrase
|
|
16
|
+
new(Crypto::Identity.identity_from_seed(Crypto::Mnemonic.phrase_to_seed(phrase)))
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def initialize(identity)
|
data/lib/mailcycle/version.rb
CHANGED