mailkite 0.3.0 → 0.4.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/lib/mailkite.rb +74 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cf95721d75808a7648a0d5c5528a5b24cebe649e4e7b4f2aa7cf96309492c4df
|
|
4
|
+
data.tar.gz: fd40da8370ad8ad24b538613a29a8b623226901a8db8e5bc6a4b0b3ebd1ec709
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 96e4a3c87afd08886338448d1edf8bfd83d7433733bba220cb892647249c923d46dde87df440ce78c84f1301b734fec480b89d9ee2e54f8eef2e528fec4af4dc
|
|
7
|
+
data.tar.gz: d7b4ccb09e09a382e7d9bef520115c50aa06c635646763b73cea18bc133326b6080371a56037b3f05ccb5461b53b9d2369d4f5bfde84dedd1e5770d0fe0443d2
|
data/lib/mailkite.rb
CHANGED
|
@@ -11,10 +11,11 @@ require "net/http"
|
|
|
11
11
|
require "uri"
|
|
12
12
|
require "json"
|
|
13
13
|
require "openssl"
|
|
14
|
+
require "base64"
|
|
14
15
|
require "cgi"
|
|
15
16
|
|
|
16
17
|
module Mailkite
|
|
17
|
-
VERSION = "0.
|
|
18
|
+
VERSION = "0.4.0"
|
|
18
19
|
DEFAULT_BASE_URL = "https://api.mailkite.dev"
|
|
19
20
|
# Reject webhook events older than this (ms) to block replays. Pass 0 to disable.
|
|
20
21
|
DEFAULT_TOLERANCE_MS = 5 * 60 * 1000
|
|
@@ -54,6 +55,65 @@ module Mailkite
|
|
|
54
55
|
diff.zero?
|
|
55
56
|
end
|
|
56
57
|
|
|
58
|
+
# The canonical body to return from a webhook handler so MailKite marks the
|
|
59
|
+
# delivery acknowledged. Returns the exact string `{"status":"ok"}`.
|
|
60
|
+
def self.reply_ok
|
|
61
|
+
'{"status":"ok"}'
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Encrypt a UTF-8 string to a customer RSA public key (SPKI/PEM), producing the
|
|
65
|
+
# MailKite at-rest envelope as a compact JSON string. Hybrid scheme: a fresh
|
|
66
|
+
# AES-256-GCM content key encrypts the data, then the raw key is wrapped with
|
|
67
|
+
# RSA-OAEP (SHA-256). Local only — no network.
|
|
68
|
+
def self.encrypt(plaintext, public_key)
|
|
69
|
+
pub = OpenSSL::PKey.read(public_key)
|
|
70
|
+
fp = OpenSSL::Digest::SHA256.hexdigest(pub.to_der)
|
|
71
|
+
|
|
72
|
+
raw_key = OpenSSL::Random.random_bytes(32)
|
|
73
|
+
iv = OpenSSL::Random.random_bytes(12)
|
|
74
|
+
|
|
75
|
+
cipher = OpenSSL::Cipher.new("aes-256-gcm")
|
|
76
|
+
cipher.encrypt
|
|
77
|
+
cipher.key = raw_key
|
|
78
|
+
cipher.iv = iv
|
|
79
|
+
body = cipher.update(plaintext.to_s.dup.force_encoding("UTF-8")) + cipher.final
|
|
80
|
+
ciphertext = body + cipher.auth_tag # GCM ct with 16-byte tag appended
|
|
81
|
+
|
|
82
|
+
wrapped = pub.encrypt(raw_key, rsa_padding_mode: "oaep", rsa_oaep_md: "sha256", rsa_mgf1_md: "sha256")
|
|
83
|
+
|
|
84
|
+
JSON.generate({
|
|
85
|
+
"v" => 1,
|
|
86
|
+
"keyAlg" => "RSA-OAEP-256",
|
|
87
|
+
"fp" => fp,
|
|
88
|
+
"enc" => "A256GCM",
|
|
89
|
+
"iv" => Base64.strict_encode64(iv),
|
|
90
|
+
"wrappedKey" => Base64.strict_encode64(wrapped),
|
|
91
|
+
"ciphertext" => Base64.strict_encode64(ciphertext),
|
|
92
|
+
})
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Decrypt a MailKite at-rest envelope (JSON string) with the matching RSA
|
|
96
|
+
# private key (PEM), returning the original UTF-8 plaintext. Local only.
|
|
97
|
+
def self.decrypt(envelope, private_key)
|
|
98
|
+
env = envelope.is_a?(String) ? JSON.parse(envelope) : envelope
|
|
99
|
+
priv = OpenSSL::PKey.read(private_key)
|
|
100
|
+
|
|
101
|
+
iv = Base64.strict_decode64(env["iv"])
|
|
102
|
+
wrapped = Base64.strict_decode64(env["wrappedKey"])
|
|
103
|
+
ct = Base64.strict_decode64(env["ciphertext"])
|
|
104
|
+
body = ct[0...-16]
|
|
105
|
+
tag = ct[-16..]
|
|
106
|
+
|
|
107
|
+
raw_key = priv.decrypt(wrapped, rsa_padding_mode: "oaep", rsa_oaep_md: "sha256", rsa_mgf1_md: "sha256")
|
|
108
|
+
|
|
109
|
+
cipher = OpenSSL::Cipher.new("aes-256-gcm")
|
|
110
|
+
cipher.decrypt
|
|
111
|
+
cipher.key = raw_key
|
|
112
|
+
cipher.iv = iv
|
|
113
|
+
cipher.auth_tag = tag
|
|
114
|
+
(cipher.update(body) + cipher.final).force_encoding("UTF-8")
|
|
115
|
+
end
|
|
116
|
+
|
|
57
117
|
class Error < StandardError
|
|
58
118
|
attr_reader :status, :body
|
|
59
119
|
|
|
@@ -204,5 +264,18 @@ module Mailkite
|
|
|
204
264
|
def verifyWebhook(signature, payload, secret, tolerance_ms = DEFAULT_TOLERANCE_MS)
|
|
205
265
|
Mailkite.verify_webhook(signature, payload, secret, tolerance_ms)
|
|
206
266
|
end
|
|
267
|
+
|
|
268
|
+
# Instance wrappers around the module-level crypto helpers. No network call.
|
|
269
|
+
def reply_ok
|
|
270
|
+
Mailkite.reply_ok
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def encrypt(plaintext, public_key)
|
|
274
|
+
Mailkite.encrypt(plaintext, public_key)
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def decrypt(envelope, private_key)
|
|
278
|
+
Mailkite.decrypt(envelope, private_key)
|
|
279
|
+
end
|
|
207
280
|
end
|
|
208
281
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mailkite
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- MailKite
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-06-
|
|
11
|
+
date: 2026-06-25 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Send and manage email over your own authenticated domain with MailKite.
|
|
14
14
|
email:
|