Class: Protocol::ZMTP::Mechanism::Curve
- Inherits:
-
Object
- Object
- Protocol::ZMTP::Mechanism::Curve
- Defined in:
- lib/protocol/zmtp/mechanism/curve.rb
Overview
CurveZMQ security mechanism (RFC 26).
Provides Curve25519-XSalsa20-Poly1305 encryption and authentication for ZMTP 3.1 connections.
Crypto-backend-agnostic: pass any module that provides the NaCl API (RbNaCl or Nuckle) via the crypto: parameter.
The crypto backend must provide:
backend::PrivateKey.new(bytes) / .generate
backend::PublicKey.new(bytes)
backend::Box.new(peer_pub, my_secret) → #encrypt(nonce, pt) / #decrypt(nonce, ct)
backend::SecretBox.new(key) → #encrypt(nonce, pt) / #decrypt(nonce, ct)
backend::Random.random_bytes(n)
backend::Util.verify32(a, b) / .verify64(a, b)
backend::CryptoError (exception class)
Constant Summary collapse
- MECHANISM_NAME =
"CURVE"- NONCE_PREFIX_HELLO =
Nonce prefixes.
"CurveZMQHELLO---"- NONCE_PREFIX_WELCOME =
"WELCOME-"- NONCE_PREFIX_INITIATE =
"CurveZMQINITIATE"- NONCE_PREFIX_READY =
"CurveZMQREADY---"- NONCE_PREFIX_MESSAGE_C =
"CurveZMQMESSAGEC"- NONCE_PREFIX_MESSAGE_S =
"CurveZMQMESSAGES"- NONCE_PREFIX_VOUCH =
"VOUCH---"- NONCE_PREFIX_COOKIE =
"COOKIE--"- BOX_OVERHEAD =
16- MAX_NONCE =
(2**64) - 1
- MESSAGE_PREFIX =
"\x07MESSAGE".b.freeze
- MESSAGE_PREFIX_SIZE =
MESSAGE_PREFIX.bytesize
Instance Attribute Summary collapse
-
#metadata ⇒ Object
Extra READY/INITIATE properties merged in by extensions (e.g. ZMTP-Zstd’s X-Compression).
Class Method Summary collapse
-
.client(server_key:, crypto:, public_key: nil, secret_key: nil) ⇒ Curve
Creates a CURVE client mechanism.
-
.server(public_key:, secret_key:, crypto:, authenticator: nil) ⇒ Curve
Creates a CURVE server mechanism.
Instance Method Summary collapse
-
#decrypt(frame) ⇒ Codec::Frame
Decrypts a CURVE MESSAGE command frame back into a plaintext frame.
-
#encrypt(body, more: false, command: false) ⇒ String
Encrypts a frame body into a CURVE MESSAGE command on the wire.
-
#encrypted? ⇒ Boolean
True – CURVE always encrypts frames.
-
#handshake!(io, as_server:, socket_type:, identity:, qos: 0, qos_hash: "") ⇒ Hash
Performs the full CurveZMQ handshake (HELLO/WELCOME/INITIATE/READY).
-
#initialize(public_key: nil, secret_key: nil, server_key: nil, crypto:, as_server: false, authenticator: nil) ⇒ Curve
constructor
A new instance of Curve.
-
#initialize_dup(source) ⇒ void
Resets session state when duplicating (e.g. for a new connection).
-
#maintenance ⇒ Hash?
Returns a periodic maintenance task for rotating the cookie key (server only).
Constructor Details
#initialize(public_key: nil, secret_key: nil, server_key: nil, crypto:, as_server: false, authenticator: nil) ⇒ Curve
Returns a new instance of Curve.
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
# File 'lib/protocol/zmtp/mechanism/curve.rb', line 78 def initialize(public_key: nil, secret_key: nil, server_key: nil, crypto:, as_server: false, authenticator: nil) @crypto = crypto @as_server = as_server @authenticator = authenticator if as_server validate_key!(public_key, "public_key") validate_key!(secret_key, "secret_key") @permanent_public = crypto::PublicKey.new(public_key.b) @permanent_secret = crypto::PrivateKey.new(secret_key.b) @cookie_key = crypto::Random.random_bytes(32) else validate_key!(server_key, "server_key") @server_public = crypto::PublicKey.new(server_key.b) if public_key && secret_key validate_key!(public_key, "public_key") validate_key!(secret_key, "secret_key") @permanent_public = crypto::PublicKey.new(public_key.b) @permanent_secret = crypto::PrivateKey.new(secret_key.b) else @permanent_secret = crypto::PrivateKey.generate @permanent_public = @permanent_secret.public_key end end @session_box = nil @send_nonce = 0 @recv_nonce = -1 @metadata = nil end |
Instance Attribute Details
#metadata ⇒ Object
Extra READY/INITIATE properties merged in by extensions (e.g. ZMTP-Zstd’s X-Compression). nil = none.
28 29 30 |
# File 'lib/protocol/zmtp/mechanism/curve.rb', line 28 def @metadata end |
Class Method Details
.client(server_key:, crypto:, public_key: nil, secret_key: nil) ⇒ Curve
Creates a CURVE client mechanism.
67 68 69 |
# File 'lib/protocol/zmtp/mechanism/curve.rb', line 67 def self.client(server_key:, crypto:, public_key: nil, secret_key: nil) new(public_key:, secret_key:, server_key:, crypto:, as_server: false) end |
.server(public_key:, secret_key:, crypto:, authenticator: nil) ⇒ Curve
Creates a CURVE server mechanism.
55 56 57 |
# File 'lib/protocol/zmtp/mechanism/curve.rb', line 55 def self.server(public_key:, secret_key:, crypto:, authenticator: nil) new(public_key:, secret_key:, crypto:, as_server: true, authenticator:) end |
Instance Method Details
#decrypt(frame) ⇒ Codec::Frame
Decrypts a CURVE MESSAGE command frame back into a plaintext frame.
194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 |
# File 'lib/protocol/zmtp/mechanism/curve.rb', line 194 def decrypt(frame) body = frame.body unless body.start_with?(MESSAGE_PREFIX) raise Error, "expected MESSAGE command" end data = body.byteslice(MESSAGE_PREFIX_SIZE..) raise Error, "MESSAGE too short" if data.bytesize < 8 + BOX_OVERHEAD short_nonce = data.byteslice(0, 8) ciphertext = data.byteslice(8..) nonce_value = short_nonce.unpack1("Q>") unless nonce_value > @recv_nonce raise Error, "MESSAGE nonce not strictly incrementing" end @recv_nonce = nonce_value @recv_nonce_buf[16, 8] = short_nonce begin plaintext = @session_box.decrypt(@recv_nonce_buf, ciphertext) rescue @crypto::CryptoError raise Error, "MESSAGE decryption failed" end flags = plaintext.getbyte(0) body = plaintext.byteslice(1..) || "".b Codec::Frame.new(body, more: (flags & 0x01) != 0, command: (flags & 0x04) != 0) end |
#encrypt(body, more: false, command: false) ⇒ String
Encrypts a frame body into a CURVE MESSAGE command on the wire.
161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 |
# File 'lib/protocol/zmtp/mechanism/curve.rb', line 161 def encrypt(body, more: false, command: false) flags = 0 flags |= 0x01 if more flags |= 0x04 if command plaintext = String.new(encoding: Encoding::BINARY, capacity: 1 + body.bytesize) plaintext << flags << body nonce = make_send_nonce ciphertext = @session_box.encrypt(nonce, plaintext) short_nonce = nonce.byteslice(16, 8) msg_body_size = 16 + ciphertext.bytesize if msg_body_size > 255 wire = String.new(encoding: Encoding::BINARY, capacity: 9 + msg_body_size) wire << "\x02" << [msg_body_size].pack("Q>") else wire = String.new(encoding: Encoding::BINARY, capacity: 2 + msg_body_size) wire << "\x00" << msg_body_size end wire << "\x07MESSAGE" << short_nonce << ciphertext end |
#encrypted? ⇒ Boolean
Returns true – CURVE always encrypts frames.
125 |
# File 'lib/protocol/zmtp/mechanism/curve.rb', line 125 def encrypted? = true |
#handshake!(io, as_server:, socket_type:, identity:, qos: 0, qos_hash: "") ⇒ Hash
Performs the full CurveZMQ handshake (HELLO/WELCOME/INITIATE/READY).
146 147 148 149 150 151 152 |
# File 'lib/protocol/zmtp/mechanism/curve.rb', line 146 def handshake!(io, as_server:, socket_type:, identity:, qos: 0, qos_hash: "") if @as_server server_handshake!(io, socket_type:, identity:, qos:, qos_hash:) else client_handshake!(io, socket_type:, identity:, qos:, qos_hash:) end end |
#initialize_dup(source) ⇒ void
This method returns an undefined value.
Resets session state when duplicating (e.g. for a new connection).
114 115 116 117 118 119 120 121 |
# File 'lib/protocol/zmtp/mechanism/curve.rb', line 114 def initialize_dup(source) super @session_box = nil @send_nonce = 0 @recv_nonce = -1 @send_nonce_buf = nil @recv_nonce_buf = nil end |
#maintenance ⇒ Hash?
Returns a periodic maintenance task for rotating the cookie key (server only).
130 131 132 133 |
# File 'lib/protocol/zmtp/mechanism/curve.rb', line 130 def maintenance return unless @as_server { interval: 60, task: -> { @cookie_key = @crypto::Random.random_bytes(32) } }.freeze end |