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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 335f0b7efcc3477484330570f7429ab5b02110bddcf744da2c22b5fc924b7dbe
4
- data.tar.gz: c119a992721686b72afa4973aa0504066d0b41cdc6c20bad031b5fce460fd1e0
3
+ metadata.gz: 00ac2000980d75a7daff3d987843ec8a7f55f8901972343322a96c73180834b4
4
+ data.tar.gz: c1f1d87eca6363dbd6d304f7fceef45ddc82ba6cb0ae0dadab8498dc84c1b8d6
5
5
  SHA512:
6
- metadata.gz: c41cce333ffe2bf99e02528ee4e95ddefea75d268d97c22a5a18474ddacb72bc6690d893e15eb6052c77f570cd2f0a99f285d490a19ae79fd19ce2b280bcf877
7
- data.tar.gz: 682bb34d733d5872529ff1bb80e223e419f52cb63e755f74e9e0f4f35512eab22b0dccd896f7123c5a3732cab1eb79d47b5cf72cd5278a0eef242e2a114c1090
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 [LICENSE](LICENSE). 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.
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.
@@ -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), `passphrase` (empty unless the account was made with one) and
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, passphrase: "", timeout: Http::DEFAULT_TIMEOUT_SECONDS)
100
- keys = Keys.from_phrase(phrase, passphrase)
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, passphrase: "",
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, passphrase)
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, passphrase: "",
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, passphrase)
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, passphrase: "",
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, passphrase)
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, passphrase = "")
94
- identity_from_seed(Mnemonic.phrase_to_seed(phrase, passphrase))
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. Stretching buys little over a
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
- salt = SEED_SALT_PREFIX.b + passphrase.to_s.b
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
@@ -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, passphrase = "")
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, passphrase)))
16
+ new(Crypto::Identity.identity_from_seed(Crypto::Mnemonic.phrase_to_seed(phrase)))
17
17
  end
18
18
 
19
19
  def initialize(identity)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Mailcycle
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mailcycle
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Northlab Studios Ltd