Class: Noise::Connection::Base

Inherits:
Object
  • Object
show all
Defined in:
lib/noise/connection/base.rb

Direct Known Subclasses

Initiator, Responder

Constant Summary collapse

MAX_MESSAGE_LENGTH =

The Noise spec caps a handshake or transport message at 65535 bytes. A transport message is the ciphertext, so the plaintext a caller may hand to encrypt is shorter by the authentication tag that ENCRYPT() appends.

65_535
MAX_PLAINTEXT_LENGTH =
MAX_MESSAGE_LENGTH - Noise::State::CipherState::TAG_LENGTH

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(name, keypairs: { s: nil, e: nil, rs: nil, re: nil }) ⇒ Base

Returns a new instance of Base.

Parameters:

  • name (String)

    the protocol name, for example 'Noise_XX_25519_ChaChaPoly_SHA256'.

  • keypairs (Hash) (defaults to: { s: nil, e: nil, rs: nil, re: nil })

    the keys the pattern needs, as private or public key strings. :s is the local static private key, :rs and :re the remote static and ephemeral public keys.

    :e sets the local ephemeral private key. It exists only so that spec/vectors_spec.rb can reproduce the official test vectors, which fix both sides' ephemeral keys to make the output deterministic. Never set it outside that use: HandshakeState#write_message reuses the keypair given here instead of generating a fresh one, and an ephemeral key reused across handshakes gives up the forward secrecy every pattern depends on. The handshake still succeeds, so nothing reports the loss.



26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/noise/connection/base.rb', line 26

def initialize(name, keypairs: { s: nil, e: nil, rs: nil, re: nil })
  @protocol = Protocol.create(name)

  # parameter keypairs[:e] and keypairs[:s] are strings, so should convert Noise::Key object.
  @local_keypairs = {}
  @local_keypairs[:e] = @protocol.dh_fn.class.from_private(keypairs[:e]) if keypairs[:e]
  @local_keypairs[:s] = @protocol.dh_fn.class.from_private(keypairs[:s]) if keypairs[:s]
  @remote_keys = { rs: keypairs[:rs], re: keypairs[:re] }
  @handshake_started = false
  @handshake_finished = false
  initialize_next_message
end

Instance Attribute Details

#cipher_state_decryptObject (readonly)

Returns the value of attribute cipher_state_decrypt.



12
13
14
# File 'lib/noise/connection/base.rb', line 12

def cipher_state_decrypt
  @cipher_state_decrypt
end

#cipher_state_encryptObject (readonly)

Returns the value of attribute cipher_state_encrypt.



12
13
14
# File 'lib/noise/connection/base.rb', line 12

def cipher_state_encrypt
  @cipher_state_encrypt
end

#cipher_state_handshakeObject (readonly)

Returns the value of attribute cipher_state_handshake.



12
13
14
# File 'lib/noise/connection/base.rb', line 12

def cipher_state_handshake
  @cipher_state_handshake
end

#handshake_finishedObject (readonly)

Returns the value of attribute handshake_finished.



12
13
14
# File 'lib/noise/connection/base.rb', line 12

def handshake_finished
  @handshake_finished
end

#handshake_hashObject (readonly)

Returns the value of attribute handshake_hash.



12
13
14
# File 'lib/noise/connection/base.rb', line 12

def handshake_hash
  @handshake_hash
end

#handshake_startedObject (readonly)

Returns the value of attribute handshake_started.



12
13
14
# File 'lib/noise/connection/base.rb', line 12

def handshake_started
  @handshake_started
end

#handshake_stateObject (readonly)

Returns the value of attribute handshake_state.



12
13
14
# File 'lib/noise/connection/base.rb', line 12

def handshake_state
  @handshake_state
end

#prologueObject

Returns the value of attribute prologue.



14
15
16
# File 'lib/noise/connection/base.rb', line 14

def prologue
  @prologue
end

#protocolObject (readonly)

Returns the value of attribute protocol.



12
13
14
# File 'lib/noise/connection/base.rb', line 12

def protocol
  @protocol
end

#psksObject

Returns the value of attribute psks.



14
15
16
# File 'lib/noise/connection/base.rb', line 14

def psks
  @psks
end

#rsObject (readonly)

Returns the value of attribute rs.



12
13
14
# File 'lib/noise/connection/base.rb', line 12

def rs
  @rs
end

#sObject (readonly)

Returns the value of attribute s.



12
13
14
# File 'lib/noise/connection/base.rb', line 12

def s
  @s
end

Instance Method Details

#decrypt(data) ⇒ Object



116
117
118
119
120
121
122
123
124
125
126
# File 'lib/noise/connection/base.rb', line 116

def decrypt(data)
  cipher_state = transport_cipher_state(:decrypt)
  # Rejected before the cipher state is used, so an over-long message leaves n untouched
  # and the connection usable, exactly as a failed decryption does.
  if data.bytesize > MAX_MESSAGE_LENGTH
    raise Noise::Exceptions::MessageTooLongError,
          "Message is #{data.bytesize} bytes, which exceeds the maximum of #{MAX_MESSAGE_LENGTH}."
  end

  cipher_state.decrypt_with_ad('', data)
end

#decryption_nonceInteger

Returns the nonce the next #decrypt call uses.

Returns:

  • (Integer)

    the nonce the next #decrypt call uses.



134
135
136
# File 'lib/noise/connection/base.rb', line 134

def decryption_nonce
  transport_cipher_state(:decrypt).n
end

#decryption_nonce=(nonce) ⇒ Object

Sets the nonce of the next #decrypt call. This is how the Noise spec handles transport messages that arrive out of order: the receiver sets n to the nonce of the message it is about to decrypt, and restores the previous value if the message fails to authenticate.

Parameters:

  • nonce (Integer)

    a value between 0 and CipherState::MAX_NONCE.



151
152
153
# File 'lib/noise/connection/base.rb', line 151

def decryption_nonce=(nonce)
  transport_cipher_state(:decrypt).nonce = nonce
end

#encrypt(data) ⇒ Object



106
107
108
109
110
111
112
113
114
# File 'lib/noise/connection/base.rb', line 106

def encrypt(data)
  cipher_state = transport_cipher_state(:encrypt)
  if data.bytesize > MAX_PLAINTEXT_LENGTH
    raise Noise::Exceptions::MessageTooLongError,
          "Plaintext is #{data.bytesize} bytes, which exceeds the maximum of #{MAX_PLAINTEXT_LENGTH}."
  end

  cipher_state.encrypt_with_ad('', data)
end

#encryption_nonceInteger

Returns the nonce the next #encrypt call uses.

Returns:

  • (Integer)

    the nonce the next #encrypt call uses.



129
130
131
# File 'lib/noise/connection/base.rb', line 129

def encryption_nonce
  transport_cipher_state(:encrypt).n
end

#encryption_nonce=(nonce) ⇒ Object

Sets the nonce of the next #encrypt call. Needed when the transport layer numbers the messages itself instead of relying on the sender and the receiver counting in step.

Parameters:

  • nonce (Integer)

    a value between 0 and CipherState::MAX_NONCE.



142
143
144
# File 'lib/noise/connection/base.rb', line 142

def encryption_nonce=(nonce)
  transport_cipher_state(:encrypt).nonce = nonce
end

#fallback(fallback_name) ⇒ Object

Restarts the handshake with a fallback pattern, carrying over the keys of the aborted one.

The roles swap here: the party that wrote the aborted message now reads, and the one that failed to read it now writes. Both sides are already in that state, so @next_message is deliberately left as it is rather than reset through initialize_next_message.



50
51
52
53
54
55
56
57
# File 'lib/noise/connection/base.rb', line 50

def fallback(fallback_name)
  @protocol = Protocol.create(fallback_name)
  @handshake_started = false
  @handshake_finished = false
  @local_keypairs = { e: @handshake_state.e, s: @handshake_state.s }
  @remote_keys = { re: @handshake_state.re, rs: @handshake_state.rs }
  start_handshake
end

#handshake_done(_c1, _c2) ⇒ Object



198
199
200
201
202
203
204
205
# File 'lib/noise/connection/base.rb', line 198

def handshake_done(_c1, _c2)
  @handshake_hash = @symmetric_state.handshake_hash
  @s = @handshake_state.s
  @rs = @handshake_state.rs
  @handshake_state = nil
  @symmetric_state = nil
  @cipher_state_handshake = nil
end

#initialise_handshake_stateObject



59
60
61
62
63
64
65
66
67
68
69
# File 'lib/noise/connection/base.rb', line 59

def initialise_handshake_state
  @handshake_state = Noise::State::HandshakeState.new(
    self,
    initiator?,
    @prologue,
    @local_keypairs,
    @remote_keys
  )
  @symmetric_state = @handshake_state.symmetric_state
  @cipher_state_handshake = @symmetric_state.cipher_state
end

#missing_keypairs?Boolean

Returns:

  • (Boolean)


185
186
187
188
# File 'lib/noise/connection/base.rb', line 185

def missing_keypairs?
  keypairs = @local_keypairs.merge(@remote_keys)
  @protocol.pattern.required_keypairs(initiator?).any? { |keypair| !keypairs[keypair] }
end

#read_message(data) ⇒ Object



89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/noise/connection/base.rb', line 89

def read_message(data)
  # Call NoiseConnection.start_handshake first
  raise Noise::Exceptions::NoiseHandshakeError unless @handshake_started
  raise Noise::Exceptions::NoiseHandshakeError if @next_message != :read
  raise Noise::Exceptions::NoiseHandshakeError if @handshake_finished

  if data.bytesize > MAX_MESSAGE_LENGTH
    raise Noise::Exceptions::MessageTooLongError,
          "Message is #{data.bytesize} bytes, which exceeds the maximum of #{MAX_MESSAGE_LENGTH}."
  end

  @next_message = :write
  buffer = +''
  @handshake_finished = @handshake_state.read_message(data, buffer)
  buffer
end

#rekey_decryptionvoid

This method returns an undefined value.

Replaces the key used by #decrypt with REKEY(k). See #rekey_encryption.



168
169
170
171
# File 'lib/noise/connection/base.rb', line 168

def rekey_decryption
  transport_cipher_state(:decrypt).rekey
  nil
end

#rekey_encryptionvoid

This method returns an undefined value.

Replaces the key used by #encrypt with REKEY(k), so that the old key cannot decrypt the messages that follow. Both parties must rekey the matching direction at the same point of the message stream, which is up to the application protocol to agree on.



160
161
162
163
# File 'lib/noise/connection/base.rb', line 160

def rekey_encryption
  transport_cipher_state(:encrypt).rekey
  nil
end

#start_handshakeObject



39
40
41
42
43
# File 'lib/noise/connection/base.rb', line 39

def start_handshake
  validate
  initialise_handshake_state
  @handshake_started = true
end

#validateObject



190
191
192
193
194
195
196
# File 'lib/noise/connection/base.rb', line 190

def validate
  validate_psk! if @protocol.psk?

  raise Noise::Exceptions::NoiseValidationError if missing_keypairs?

  true
end

#validate_psk!Object



173
174
175
176
177
178
179
180
181
182
183
# File 'lib/noise/connection/base.rb', line 173

def validate_psk!
  raise Noise::Exceptions::NoisePSKError, 'psks are not set.' if @psks.nil?
  # Invalid psk length! Has to be 32 bytes long
  raise Noise::Exceptions::NoisePSKError, 'psks have to be 32 bytes long.' if
    @psks.any? { |psk| psk.bytesize != 32 }

  return if @protocol.pattern.psk_count == @psks.count

  raise Noise::Exceptions::NoisePSKError,
        "This protocol needs #{@protocol.pattern.psk_count} psks, got #{@psks.count}."
end

#write_message(payload = '') ⇒ Object



71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/noise/connection/base.rb', line 71

def write_message(payload = '')
  # Call NoiseConnection.start_handshake first
  raise Noise::Exceptions::NoiseHandshakeError unless @handshake_started
  raise Noise::Exceptions::NoiseHandshakeError if @next_message != :write
  raise Noise::Exceptions::NoiseHandshakeError if @handshake_finished

  length = @handshake_state.expected_message_length(payload.bytesize)
  if length > MAX_MESSAGE_LENGTH
    raise Noise::Exceptions::MessageTooLongError,
          "Message would be #{length} bytes, which exceeds the maximum of #{MAX_MESSAGE_LENGTH}."
  end

  @next_message = :read
  buffer = +''
  @handshake_finished = @handshake_state.write_message(payload, buffer)
  buffer
end