mirror of
https://github.com/logos-messaging/go-noise.git
synced 2026-01-10 00:43:08 +00:00
feat: initial commit
This commit is contained in:
commit
aedffc51a8
205
LICENSE-APACHEv2
Normal file
205
LICENSE-APACHEv2
Normal file
@ -0,0 +1,205 @@
|
||||
nim-waku is licensed under the Apache License version 2
|
||||
Copyright (c) 2018 Status Research & Development GmbH
|
||||
-----------------------------------------------------
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2018 Status Research & Development GmbH
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
25
LICENSE-MIT
Normal file
25
LICENSE-MIT
Normal file
@ -0,0 +1,25 @@
|
||||
nim-waku is licensed under the MIT License
|
||||
Copyright (c) 2018 Status Research & Development GmbH
|
||||
-----------------------------------------------------
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018 Status Research & Development GmbH
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
4
README.md
Normal file
4
README.md
Normal file
@ -0,0 +1,4 @@
|
||||
# go-noise
|
||||
A Go implementation for Waku Noise ([35/WAKU2-NOISE](https://rfc.vac.dev/spec/35/), [37/WAKU2-NOISE-SESSIONS](https://rfc.vac.dev/spec/37/), [43/WAKU2-NOISE-PAIRING](https://rfc.vac.dev/spec/43/)).
|
||||
|
||||
|
||||
46
cipher.go
Normal file
46
cipher.go
Normal file
@ -0,0 +1,46 @@
|
||||
package noise
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"hash"
|
||||
"io"
|
||||
|
||||
"golang.org/x/crypto/hkdf"
|
||||
)
|
||||
|
||||
type DHKey interface {
|
||||
GenerateKeypair() (Keypair, error)
|
||||
DH(privkey, pubkey []byte) ([]byte, error)
|
||||
DHLen() int
|
||||
}
|
||||
|
||||
type Keypair struct {
|
||||
Private []byte
|
||||
Public []byte
|
||||
}
|
||||
|
||||
func (k Keypair) IsDefault() bool {
|
||||
return k.Equals(Keypair{})
|
||||
}
|
||||
|
||||
func (k Keypair) Equals(b Keypair) bool {
|
||||
return bytes.Equal(k.Private, b.Private) && bytes.Equal(k.Public, b.Public)
|
||||
}
|
||||
|
||||
func getHKDF(h func() hash.Hash, ck []byte, ikm []byte, numBytes int) []byte {
|
||||
hkdf := hkdf.New(h, ikm, ck, nil)
|
||||
result := make([]byte, numBytes)
|
||||
_, _ = io.ReadFull(hkdf, result)
|
||||
return result
|
||||
}
|
||||
|
||||
// CommitPublicKey commits a public key pk for randomness r as H(pk || s)
|
||||
func CommitPublicKey(h func() hash.Hash, publicKey []byte, r []byte) []byte {
|
||||
input := []byte{}
|
||||
input = append(input, []byte(publicKey)...)
|
||||
input = append(input, r...)
|
||||
|
||||
hash := h()
|
||||
hash.Write(input)
|
||||
return hash.Sum(nil)
|
||||
}
|
||||
36
curve25519.go
Normal file
36
curve25519.go
Normal file
@ -0,0 +1,36 @@
|
||||
package noise
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"io"
|
||||
|
||||
"golang.org/x/crypto/curve25519"
|
||||
)
|
||||
|
||||
type dh25519 struct {
|
||||
DHKey
|
||||
}
|
||||
|
||||
func (d dh25519) GenerateKeypair() (Keypair, error) {
|
||||
privkey := make([]byte, DH25519.DHLen())
|
||||
if _, err := io.ReadFull(rand.Reader, privkey); err != nil {
|
||||
return Keypair{}, err
|
||||
}
|
||||
return d.GenerateKeyPairFromPrivateKey(privkey)
|
||||
}
|
||||
|
||||
func (d dh25519) DH(privkey, pubkey []byte) ([]byte, error) {
|
||||
return curve25519.X25519(privkey, pubkey)
|
||||
}
|
||||
|
||||
func (d dh25519) DHLen() int { return 32 }
|
||||
|
||||
func (d dh25519) GenerateKeyPairFromPrivateKey(privkey []byte) (Keypair, error) {
|
||||
pubkey, err := curve25519.X25519(privkey, curve25519.Basepoint)
|
||||
if err != nil {
|
||||
return Keypair{}, err
|
||||
}
|
||||
return Keypair{Private: privkey, Public: pubkey}, nil
|
||||
}
|
||||
|
||||
var DH25519 = dh25519{}
|
||||
15
go.mod
Normal file
15
go.mod
Normal file
@ -0,0 +1,15 @@
|
||||
module github.com/waku-org/go-noise
|
||||
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/stretchr/testify v1.8.1
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
25
go.sum
Normal file
25
go.sum
Normal file
@ -0,0 +1,25 @@
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w=
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
289
handshake.go
Normal file
289
handshake.go
Normal file
@ -0,0 +1,289 @@
|
||||
package noise
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
// Noise state machine
|
||||
|
||||
var ErrUnexpectedMessageNametag = errors.New("the message nametag of the read message doesn't match the expected one")
|
||||
var ErrHandshakeComplete = errors.New("handshake complete")
|
||||
|
||||
// While processing messages patterns, users either:
|
||||
// - read (decrypt) the other party's (encrypted) transport message
|
||||
// - write (encrypt) a message, sent through a PayloadV2
|
||||
// These two intermediate results are stored in the HandshakeStepResult data structure
|
||||
|
||||
// HandshakeStepResult stores the intermediate result of processing messages patterns
|
||||
type HandshakeStepResult struct {
|
||||
PayloadV2 *PayloadV2
|
||||
TransportMessage []byte
|
||||
}
|
||||
|
||||
// When a handshake is complete, the HandshakeResult will contain the two
|
||||
// Cipher States used to encrypt/decrypt outbound/inbound messages
|
||||
// The recipient static key rs and handshake hash values h are stored to address some possible future applications (channel-binding, session management, etc.).
|
||||
// However, are not required by Noise specifications and are thus optional
|
||||
type HandshakeResult struct {
|
||||
csOutbound *CipherState
|
||||
csInbound *CipherState
|
||||
|
||||
// Optional fields:
|
||||
nametagsInbound MessageNametagBuffer
|
||||
nametagsOutbound MessageNametagBuffer
|
||||
rs []byte
|
||||
h []byte
|
||||
}
|
||||
|
||||
func NewHandshakeResult(csOutbound *CipherState, csInbound *CipherState) *HandshakeResult {
|
||||
return &HandshakeResult{
|
||||
csInbound: csInbound,
|
||||
csOutbound: csOutbound,
|
||||
}
|
||||
}
|
||||
|
||||
// Noise specification, Section 5:
|
||||
// Transport messages are then encrypted and decrypted by calling EncryptWithAd()
|
||||
// and DecryptWithAd() on the relevant CipherState with zero-length associated data.
|
||||
// If DecryptWithAd() signals an error due to DECRYPT() failure, then the input message is discarded.
|
||||
// The application may choose to delete the CipherState and terminate the session on such an error,
|
||||
// or may continue to attempt communications. If EncryptWithAd() or DecryptWithAd() signal an error
|
||||
// due to nonce exhaustion, then the application must delete the CipherState and terminate the session.
|
||||
|
||||
// Writes an encrypted message using the proper Cipher State
|
||||
func (hr *HandshakeResult) WriteMessage(transportMessage []byte, outboundMessageNametagBuffer *MessageNametagBuffer) (*PayloadV2, error) {
|
||||
payload2 := &PayloadV2{}
|
||||
|
||||
// We set the message nametag using the input buffer
|
||||
if outboundMessageNametagBuffer != nil {
|
||||
payload2.MessageNametag = outboundMessageNametagBuffer.Pop()
|
||||
} else {
|
||||
payload2.MessageNametag = hr.nametagsOutbound.Pop()
|
||||
}
|
||||
|
||||
// According to 35/WAKU2-NOISE RFC, no Handshake protocol information is sent when exchanging messages
|
||||
// This correspond to setting protocol-id to 0
|
||||
payload2.ProtocolId = 0
|
||||
// We pad the transport message
|
||||
paddedTransportMessage, err := PKCS7_Pad(transportMessage, NoisePaddingBlockSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Encryption is done with zero-length associated data as per specification
|
||||
transportMessage, err = hr.csOutbound.encryptWithAd(payload2.MessageNametag[:], paddedTransportMessage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
payload2.TransportMessage = transportMessage
|
||||
|
||||
return payload2, nil
|
||||
}
|
||||
|
||||
// Reads an encrypted message using the proper Cipher State
|
||||
// Decryption is attempted only if the input PayloadV2 has a messageNametag equal to the one expected
|
||||
func (hr *HandshakeResult) ReadMessage(readPayload2 *PayloadV2, inboundMessageNametagBuffer *MessageNametagBuffer) ([]byte, error) {
|
||||
// The output decrypted message
|
||||
var message []byte
|
||||
|
||||
// If the message nametag does not correspond to the nametag expected in the inbound message nametag buffer
|
||||
// an error is raised (to be handled externally, i.e. re-request lost messages, discard, etc.)
|
||||
if inboundMessageNametagBuffer != nil {
|
||||
err := inboundMessageNametagBuffer.CheckNametag(readPayload2.MessageNametag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
err := hr.nametagsInbound.CheckNametag(readPayload2.MessageNametag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// At this point the messageNametag matches the expected nametag.
|
||||
// According to 35/WAKU2-NOISE RFC, no Handshake protocol information is sent when exchanging messages
|
||||
if readPayload2.ProtocolId == 0 {
|
||||
// Decryption is done with messageNametag as associated data
|
||||
paddedMessage, err := hr.csInbound.decryptWithAd(readPayload2.MessageNametag[:], readPayload2.TransportMessage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We unpad the decrypted message
|
||||
message, err = PKCS7_Unpad(paddedMessage, NoisePaddingBlockSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// The message successfully decrypted, we can delete the first element of the inbound Message Nametag Buffer
|
||||
hr.nametagsInbound.Delete(1)
|
||||
}
|
||||
|
||||
return message, nil
|
||||
}
|
||||
|
||||
type Handshake struct {
|
||||
hs *HandshakeState
|
||||
hsResult *HandshakeResult
|
||||
}
|
||||
|
||||
func NewHandshake(hsPattern HandshakePattern, staticKey Keypair, ephemeralKey Keypair, prologue []byte, psk []byte, preMessagePKs []*NoisePublicKey, initiator bool) (*Handshake, error) {
|
||||
result := &Handshake{}
|
||||
result.hs = NewHandshakeState(hsPattern, psk)
|
||||
result.hs.ss.mixHash(prologue)
|
||||
result.hs.e = ephemeralKey
|
||||
result.hs.s = staticKey
|
||||
result.hs.psk = psk
|
||||
result.hs.msgPatternIdx = 0
|
||||
result.hs.initiator = initiator
|
||||
|
||||
// We process any eventual handshake pre-message pattern by processing pre-message public keys
|
||||
err := result.hs.processPreMessagePatternTokens(preMessagePKs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (h *Handshake) Equals(b *Handshake) bool {
|
||||
return h.hs.Equals(*b.hs)
|
||||
}
|
||||
|
||||
// Uses the cryptographic information stored in the input handshake state to generate a random message nametag
|
||||
// In current implementation the messageNametag = HKDF(handshake hash value), but other derivation mechanisms can be implemented
|
||||
func (hs *Handshake) ToMessageNametag() (MessageNametag, error) {
|
||||
output := getHKDF(hs.hs.handshakePattern.hashFn, hs.hs.ss.h, nil, 16)
|
||||
return BytesToMessageNametag(output), nil
|
||||
}
|
||||
|
||||
// Generates an 8 decimal digits authorization code using HKDF and the handshake state
|
||||
func (h *Handshake) Authcode() (string, error) {
|
||||
output0 := getHKDF(h.hs.handshakePattern.hashFn, h.hs.ss.h, nil, 8)
|
||||
bn := new(big.Int)
|
||||
bn.SetBytes(output0)
|
||||
code := new(big.Int)
|
||||
code.Mod(bn, big.NewInt(100_000_000))
|
||||
return fmt.Sprintf("'%08s'", code.String()), nil
|
||||
}
|
||||
|
||||
// Advances 1 step in handshake
|
||||
// Each user in a handshake alternates writing and reading of handshake messages.
|
||||
// If the user is writing the handshake message, the transport message (if not empty) and eventually a non-empty message nametag has to be passed to transportMessage and messageNametag and readPayloadV2 can be left to its default value
|
||||
// It the user is reading the handshake message, the read payload v2 has to be passed to readPayloadV2 and the transportMessage can be left to its default values. Decryption is skipped if the PayloadV2 read doesn't have a message nametag equal to messageNametag (empty input nametags are converted to all-0 MessageNametagLength bytes arrays)
|
||||
func (h *Handshake) Step(readPayloadV2 *PayloadV2, transportMessage []byte, messageNametag MessageNametag) (*HandshakeStepResult, error) {
|
||||
hsStepResult := &HandshakeStepResult{}
|
||||
|
||||
if h.IsComplete() {
|
||||
return nil, ErrHandshakeComplete
|
||||
}
|
||||
|
||||
// We process the next handshake message pattern
|
||||
|
||||
// We get if the user is reading or writing the input handshake message
|
||||
direction := h.hs.handshakePattern.messagePatterns[h.hs.msgPatternIdx].direction
|
||||
reading, writing := h.hs.getReadingWritingState(direction)
|
||||
|
||||
var err error
|
||||
|
||||
if writing { // If we write an answer at this handshake step
|
||||
hsStepResult.PayloadV2 = &PayloadV2{}
|
||||
hsStepResult.PayloadV2.ProtocolId = h.hs.handshakePattern.protocolID
|
||||
|
||||
// We set the messageNametag and the handshake and transport messages
|
||||
hsStepResult.PayloadV2.MessageNametag = messageNametag
|
||||
hsStepResult.PayloadV2.HandshakeMessage, err = h.hs.processMessagePatternTokens(nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We write the payload by passing the messageNametag as extra additional data
|
||||
hsStepResult.PayloadV2.TransportMessage, err = h.hs.processMessagePatternPayload(transportMessage, hsStepResult.PayloadV2.MessageNametag[:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
} else if reading { // If we read an answer during this handshake step
|
||||
// If the read message nametag doesn't match the expected input one we raise an error
|
||||
expectedNametag := messageNametag
|
||||
if !bytes.Equal(readPayloadV2.MessageNametag[:], expectedNametag[:]) {
|
||||
return nil, ErrUnexpectedMessageNametag
|
||||
}
|
||||
|
||||
// We process the read public keys and (eventually decrypt) the read transport message
|
||||
// Since we only read, nothing meaningful (i.e. public keys) is returned
|
||||
_, err := h.hs.processMessagePatternTokens(readPayloadV2.HandshakeMessage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// We retrieve and store the (decrypted) received transport message by passing the messageNametag as extra additional data
|
||||
hsStepResult.TransportMessage, err = h.hs.processMessagePatternPayload(readPayloadV2.TransportMessage, readPayloadV2.MessageNametag[:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
return nil, errors.New("handshake Error: neither writing or reading user")
|
||||
}
|
||||
|
||||
// We increase the handshake state message pattern index to progress to next step
|
||||
h.hs.msgPatternIdx += 1
|
||||
|
||||
return hsStepResult, nil
|
||||
}
|
||||
|
||||
// Finalizes the handshake by calling Split and assigning the proper Cipher States to users
|
||||
func (h *Handshake) FinalizeHandshake() (*HandshakeResult, error) {
|
||||
if h.IsComplete() {
|
||||
return h.hsResult, nil
|
||||
}
|
||||
|
||||
var hsResult *HandshakeResult
|
||||
|
||||
// Noise specification, Section 5:
|
||||
// Processing the final handshake message returns two CipherState objects,
|
||||
// the first for encrypting transport messages from initiator to responder,
|
||||
// and the second for messages in the other direction.
|
||||
|
||||
// We call Split()
|
||||
cs1, cs2 := h.hs.ss.split()
|
||||
|
||||
// Optional: We derive a secret for the nametag derivation
|
||||
nms1, nms2 := h.hs.genMessageNametagSecrets()
|
||||
|
||||
// We assign the proper Cipher States
|
||||
if h.hs.initiator {
|
||||
hsResult = NewHandshakeResult(cs1, cs2)
|
||||
// and nametags secrets
|
||||
hsResult.nametagsInbound.secret = nms1
|
||||
hsResult.nametagsOutbound.secret = nms2
|
||||
} else {
|
||||
hsResult = NewHandshakeResult(cs2, cs1)
|
||||
// and nametags secrets
|
||||
hsResult.nametagsInbound.secret = nms2
|
||||
hsResult.nametagsOutbound.secret = nms1
|
||||
}
|
||||
|
||||
// We initialize the message nametags inbound/outbound buffers
|
||||
hsResult.nametagsInbound.Init()
|
||||
hsResult.nametagsOutbound.Init()
|
||||
|
||||
if len(h.hs.rs) == 0 {
|
||||
return nil, errors.New("invalid handshake state")
|
||||
}
|
||||
|
||||
// We store the optional fields rs and h
|
||||
copy(hsResult.rs[:], h.hs.rs)
|
||||
copy(hsResult.h[:], h.hs.ss.h)
|
||||
|
||||
h.hsResult = hsResult
|
||||
|
||||
return hsResult, nil
|
||||
}
|
||||
|
||||
// HandshakeComplete indicates whether the handshake process is complete or not
|
||||
func (hs *Handshake) IsComplete() bool {
|
||||
return hs.hsResult != nil
|
||||
}
|
||||
543
handshake_state.go
Normal file
543
handshake_state.go
Normal file
@ -0,0 +1,543 @@
|
||||
package noise
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// The padding blocksize of a transport message
|
||||
const NoisePaddingBlockSize = 248
|
||||
|
||||
// The Handshake State as in https://noiseprotocol.org/noise.html#the-handshakestate-object
|
||||
// Contains
|
||||
// - the local and remote ephemeral/static keys e,s,re,rs (if any)
|
||||
// - the initiator flag (true if the user creating the state is the handshake initiator, false otherwise)
|
||||
// - the handshakePattern (containing the handshake protocol name, and (pre)message patterns)
|
||||
// This object is further extended from specifications by storing:
|
||||
// - a message pattern index msgPatternIdx indicating the next handshake message pattern to process
|
||||
// - the user's preshared psk, if any
|
||||
type HandshakeState struct {
|
||||
s Keypair
|
||||
e Keypair
|
||||
rs []byte
|
||||
re []byte
|
||||
ss *SymmetricState
|
||||
initiator bool
|
||||
handshakePattern HandshakePattern
|
||||
msgPatternIdx int
|
||||
psk []byte
|
||||
}
|
||||
|
||||
func NewHandshakeState(hsPattern HandshakePattern, psk []byte) *HandshakeState {
|
||||
return &HandshakeState{
|
||||
// By default the Handshake State initiator flag is set to false
|
||||
// Will be set to true when the user associated to the handshake state starts an handshake
|
||||
initiator: false,
|
||||
handshakePattern: hsPattern,
|
||||
psk: psk,
|
||||
ss: NewSymmetricState(hsPattern),
|
||||
msgPatternIdx: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HandshakeState) Equals(b HandshakeState) bool {
|
||||
if !bytes.Equal(h.s.Private, b.s.Private) {
|
||||
return false
|
||||
}
|
||||
if !bytes.Equal(h.s.Public, b.s.Public) {
|
||||
return false
|
||||
}
|
||||
if !bytes.Equal(h.e.Private, b.e.Private) {
|
||||
return false
|
||||
}
|
||||
if !bytes.Equal(h.e.Public, b.e.Public) {
|
||||
return false
|
||||
}
|
||||
|
||||
if !bytes.Equal(h.rs, b.rs) {
|
||||
return false
|
||||
}
|
||||
|
||||
if !bytes.Equal(h.re, b.re) {
|
||||
return false
|
||||
}
|
||||
|
||||
if !h.ss.Equals(b.ss) {
|
||||
return false
|
||||
}
|
||||
|
||||
if h.initiator != b.initiator {
|
||||
return false
|
||||
}
|
||||
|
||||
if !h.handshakePattern.Equals(b.handshakePattern) {
|
||||
return false
|
||||
}
|
||||
|
||||
if h.msgPatternIdx != b.msgPatternIdx {
|
||||
return false
|
||||
}
|
||||
|
||||
if !bytes.Equal(h.psk, b.psk) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (h *HandshakeState) genMessageNametagSecrets() (nms1 []byte, nms2 []byte) {
|
||||
keyLen := h.handshakePattern.hashFn().Size()
|
||||
output := getHKDF(h.handshakePattern.hashFn, h.ss.h, []byte{}, keyLen*2)
|
||||
nms1 = output[:keyLen]
|
||||
nms2 = output[keyLen:]
|
||||
return
|
||||
}
|
||||
|
||||
// Uses the cryptographic information stored in the input handshake state to generate a random message nametag
|
||||
// In current implementation the messageNametag = HKDF(handshake hash value), but other derivation mechanisms can be implemented
|
||||
func (h *HandshakeState) MessageNametag() MessageNametag {
|
||||
output := getHKDF(h.handshakePattern.hashFn, h.ss.h, []byte{}, MessageNametagLength)
|
||||
return BytesToMessageNametag(output)
|
||||
}
|
||||
|
||||
// Handshake Processing
|
||||
|
||||
// Based on the message handshake direction and if the user is or not the initiator, returns a boolean tuple telling if the user
|
||||
// has to read or write the next handshake message
|
||||
func (h *HandshakeState) getReadingWritingState(direction MessageDirection) (reading bool, writing bool) {
|
||||
if h.initiator && direction == Right {
|
||||
// I'm Alice and direction is ->
|
||||
writing = true
|
||||
} else if h.initiator && direction == Left {
|
||||
// I'm Alice and direction is <-
|
||||
reading = true
|
||||
} else if !h.initiator && direction == Right {
|
||||
// I'm Bob and direction is ->
|
||||
reading = true
|
||||
} else if !h.initiator && direction == Left {
|
||||
// I'm Bob and direction is <-
|
||||
writing = true
|
||||
}
|
||||
return reading, writing
|
||||
}
|
||||
|
||||
// Checks if a pre-message is valid according to Noise specifications
|
||||
// http://www.noiseprotocol.org/noise.html#handshake-patterns
|
||||
func (h *HandshakeState) isValid(msg []PreMessagePattern) bool {
|
||||
// Non-empty pre-messages can only have patterns "e", "s", "e,s" in each direction
|
||||
allowedPatterns := []PreMessagePattern{
|
||||
NewPreMessagePattern(Right, []NoiseTokens{S}),
|
||||
NewPreMessagePattern(Right, []NoiseTokens{E}),
|
||||
NewPreMessagePattern(Right, []NoiseTokens{E, S}),
|
||||
NewPreMessagePattern(Left, []NoiseTokens{S}),
|
||||
NewPreMessagePattern(Left, []NoiseTokens{E}),
|
||||
NewPreMessagePattern(Left, []NoiseTokens{E, S}),
|
||||
}
|
||||
|
||||
// We check if pre message patterns are allowed
|
||||
for _, p := range msg {
|
||||
found := false
|
||||
for _, allowed := range allowedPatterns {
|
||||
if allowed.Equals(p) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Handshake messages processing procedures
|
||||
|
||||
// Processes pre-message patterns
|
||||
func (h *HandshakeState) processPreMessagePatternTokens(inPreMessagePKs []*NoisePublicKey) error {
|
||||
// I make a copy of the input pre-message public keys, so that I can easily delete processed ones without using iterators/counters
|
||||
preMessagePKs := append([]*NoisePublicKey(nil), inPreMessagePKs...)
|
||||
|
||||
// Here we store currently processed pre message public key
|
||||
var currPK *NoisePublicKey
|
||||
|
||||
// We retrieve the pre-message patterns to process, if any
|
||||
// If none, there's nothing to do
|
||||
if len(h.handshakePattern.premessagePatterns) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// If not empty, we check that pre-message is valid according to Noise specifications
|
||||
if !h.isValid(h.handshakePattern.premessagePatterns) {
|
||||
return errors.New("invalid pre-message in handshake")
|
||||
}
|
||||
|
||||
// We iterate over each pattern contained in the pre-message
|
||||
for _, messagePattern := range h.handshakePattern.premessagePatterns {
|
||||
direction := messagePattern.direction
|
||||
tokens := messagePattern.tokens
|
||||
|
||||
// We get if the user is reading or writing the current pre-message pattern
|
||||
reading, writing := h.getReadingWritingState(direction)
|
||||
|
||||
// We process each message pattern token
|
||||
for _, token := range tokens {
|
||||
// We process the pattern token
|
||||
switch token {
|
||||
case E:
|
||||
// We expect an ephemeral key, so we attempt to read it (next PK to process will always be at index 0 of preMessagePKs)
|
||||
if len(preMessagePKs) > 0 {
|
||||
currPK = preMessagePKs[0]
|
||||
} else {
|
||||
return errors.New("noise pre-message read e, expected a public key")
|
||||
}
|
||||
|
||||
// If user is reading the "e" token
|
||||
if reading {
|
||||
// We check if current key is encrypted or not. We assume pre-message public keys are all unencrypted on users' end
|
||||
if currPK.Flag == 0 {
|
||||
// Sets re and calls MixHash(re.public_key).
|
||||
h.re = currPK.Public
|
||||
h.ss.mixHash(h.re)
|
||||
} else {
|
||||
return errors.New("noise read e, incorrect encryption flag for pre-message public key")
|
||||
}
|
||||
// If user is writing the "e" token
|
||||
} else if writing {
|
||||
// When writing, the user is sending a public key,
|
||||
// We check that the public part corresponds to the set local key and we call MixHash(e.public_key).
|
||||
if bytes.Equal(h.e.Public, currPK.Public) {
|
||||
h.ss.mixHash(h.e.Public)
|
||||
} else {
|
||||
return errors.New("noise pre-message e key doesn't correspond to locally set e key pair")
|
||||
}
|
||||
}
|
||||
|
||||
// Noise specification: section 9.2
|
||||
// In non-PSK handshakes, the "e" token in a pre-message pattern or message pattern always results
|
||||
// in a call to MixHash(e.public_key).
|
||||
// In a PSK handshake, all of these calls are followed by MixKey(e.public_key).
|
||||
if strings.Contains(h.handshakePattern.name, string(PSK)) {
|
||||
h.ss.mixKey(currPK.Public)
|
||||
}
|
||||
|
||||
// We delete processed public key
|
||||
preMessagePKs = preMessagePKs[1:]
|
||||
case S:
|
||||
// We expect a static key, so we attempt to read it (next PK to process will always be at index of preMessagePKs)
|
||||
if len(preMessagePKs) > 0 {
|
||||
currPK = preMessagePKs[0]
|
||||
} else {
|
||||
return errors.New("noise pre-message read s, expected a public key")
|
||||
}
|
||||
|
||||
// If user is reading the "s" token
|
||||
if reading {
|
||||
// We check if current key is encrypted or not. We assume pre-message public keys are all unencrypted on users' end
|
||||
if currPK.Flag == 0 {
|
||||
// Sets rs and calls MixHash(rs.public_key).
|
||||
h.rs = currPK.Public
|
||||
h.ss.mixHash(h.rs)
|
||||
} else {
|
||||
return errors.New("noise read s, incorrect encryption flag for pre-message public key")
|
||||
}
|
||||
|
||||
// If user is writing the "s" token
|
||||
} else if writing {
|
||||
// If writing, it means that the user is sending a public key,
|
||||
// We check that the public part corresponds to the set local key and we call MixHash(s.public_key).
|
||||
if bytes.Equal(h.s.Public, currPK.Public) {
|
||||
h.ss.mixHash(h.s.Public)
|
||||
} else {
|
||||
return errors.New("noise pre-message s key doesn't correspond to locally set s key pair")
|
||||
}
|
||||
}
|
||||
|
||||
// Noise specification: section 9.2
|
||||
// In non-PSK handshakes, the "e" token in a pre-message pattern or message pattern always results
|
||||
// in a call to MixHash(e.public_key).
|
||||
// In a PSK handshake, all of these calls are followed by MixKey(e.public_key).
|
||||
if strings.Contains(h.handshakePattern.name, string(PSK)) {
|
||||
h.ss.mixKey(currPK.Public)
|
||||
}
|
||||
|
||||
// We delete processed public key
|
||||
preMessagePKs = preMessagePKs[1:]
|
||||
default:
|
||||
return errors.New("invalid Token for pre-message pattern")
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// This procedure encrypts/decrypts the implicit payload attached at the end of every message pattern
|
||||
// An optional extraAd to pass extra additional data in encryption/decryption can be set (useful to authenticate messageNametag)
|
||||
func (h *HandshakeState) processMessagePatternPayload(transportMessage []byte, extraAd []byte) ([]byte, error) {
|
||||
var payload []byte
|
||||
var err error
|
||||
|
||||
// We retrieve current message pattern (direction + tokens) to process
|
||||
direction := h.handshakePattern.messagePatterns[h.msgPatternIdx].direction
|
||||
|
||||
// We get if the user is reading or writing the input handshake message
|
||||
reading, writing := h.getReadingWritingState(direction)
|
||||
|
||||
// We decrypt the transportMessage, if any
|
||||
if reading {
|
||||
payload, err = h.ss.decryptAndHash(transportMessage, extraAd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payload, err = PKCS7_Unpad(payload, NoisePaddingBlockSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if writing {
|
||||
payload, err = PKCS7_Pad(transportMessage, NoisePaddingBlockSize)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payload, err = h.ss.encryptAndHash(payload, extraAd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
return nil, errors.New("undefined state")
|
||||
}
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
// We process an input handshake message according to current handshake state and we return the next handshake step's handshake message
|
||||
func (h *HandshakeState) processMessagePatternTokens(inputHandshakeMessage []*NoisePublicKey) ([]*NoisePublicKey, error) {
|
||||
// We retrieve current message pattern (direction + tokens) to process
|
||||
messagePattern := h.handshakePattern.messagePatterns[h.msgPatternIdx]
|
||||
direction := messagePattern.direction
|
||||
tokens := messagePattern.tokens
|
||||
|
||||
// We get if the user is reading or writing the input handshake message
|
||||
reading, writing := h.getReadingWritingState(direction)
|
||||
|
||||
// I make a copy of the handshake message so that I can easily delete processed PKs without using iterators/counters
|
||||
// (Possibly) non-empty if reading
|
||||
inHandshakeMessage := append([]*NoisePublicKey(nil), inputHandshakeMessage...)
|
||||
|
||||
// The party's output public keys
|
||||
// (Possibly) non-empty if writing
|
||||
var outHandshakeMessage []*NoisePublicKey
|
||||
|
||||
// In currPK we store the currently processed public key from the handshake message
|
||||
var currPK *NoisePublicKey
|
||||
|
||||
// We process each message pattern token
|
||||
for _, token := range tokens {
|
||||
switch token {
|
||||
case E:
|
||||
// If user is reading the "s" token
|
||||
if reading {
|
||||
// We expect an ephemeral key, so we attempt to read it (next PK to process will always be at index 0 of preMessagePKs)
|
||||
if len(inHandshakeMessage) > 0 {
|
||||
currPK = inHandshakeMessage[0]
|
||||
} else {
|
||||
return nil, errors.New("noise read e, expected a public key")
|
||||
}
|
||||
|
||||
// We check if current key is encrypted or not
|
||||
// Note: by specification, ephemeral keys should always be unencrypted. But we support encrypted ones.
|
||||
if currPK.Flag == 0 {
|
||||
// Unencrypted Public Key
|
||||
// Sets re and calls MixHash(re.public_key).
|
||||
h.re = currPK.Public
|
||||
h.ss.mixHash(h.re)
|
||||
|
||||
// The following is out of specification: we call decryptAndHash for encrypted ephemeral keys, similarly as happens for (encrypted) static keys
|
||||
} else if currPK.Flag == 1 {
|
||||
// Encrypted public key
|
||||
// Decrypts re, sets re and calls MixHash(re.public_key).
|
||||
decRe, err := h.ss.decryptAndHash(currPK.Public, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h.re = decRe
|
||||
} else {
|
||||
return nil, errors.New("noise read e, incorrect encryption flag for public key")
|
||||
}
|
||||
|
||||
// Noise specification: section 9.2
|
||||
// In non-PSK handshakes, the "e" token in a pre-message pattern or message pattern always results
|
||||
// in a call to MixHash(e.public_key).
|
||||
// In a PSK handshake, all of these calls are followed by MixKey(e.public_key).
|
||||
if strings.Contains(h.handshakePattern.name, string(PSK)) {
|
||||
h.ss.mixKey(h.re)
|
||||
}
|
||||
|
||||
// We delete processed public key
|
||||
inHandshakeMessage = inHandshakeMessage[1:]
|
||||
|
||||
// If user is writing the "e" token
|
||||
} else if writing {
|
||||
// We generate a new ephemeral keypair
|
||||
e, err := DH25519.GenerateKeypair()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h.e = e
|
||||
|
||||
// We update the state
|
||||
h.ss.mixHash(h.e.Public)
|
||||
|
||||
// Noise specification: section 9.2
|
||||
// In non-PSK handshakes, the "e" token in a pre-message pattern or message pattern always results
|
||||
// in a call to MixHash(e.public_key).
|
||||
// In a PSK handshake, all of these calls are followed by MixKey(e.public_key).
|
||||
if strings.Contains(h.handshakePattern.name, string(PSK)) {
|
||||
h.ss.mixKey(h.e.Public)
|
||||
}
|
||||
|
||||
// We add the ephemeral public key to the Waku payload
|
||||
outHandshakeMessage = append(outHandshakeMessage, byteToNoisePublicKey(h.handshakePattern.dhKey, h.e.Public))
|
||||
}
|
||||
case S:
|
||||
// If user is reading the "s" token
|
||||
if reading {
|
||||
// We expect a static key, so we attempt to read it (next PK to process will always be at index 0 of preMessagePKs)
|
||||
if len(inHandshakeMessage) > 0 {
|
||||
currPK = inHandshakeMessage[0]
|
||||
} else {
|
||||
return nil, errors.New("noise read s, expected a public key")
|
||||
}
|
||||
|
||||
// We check if current key is encrypted or not
|
||||
if currPK.Flag == 0 {
|
||||
// Unencrypted Public Key
|
||||
// Sets re and calls MixHash(re.public_key).
|
||||
h.rs = currPK.Public
|
||||
h.ss.mixHash(h.rs)
|
||||
} else if currPK.Flag == 1 {
|
||||
// Encrypted public key
|
||||
// Decrypts rs, sets rs and calls MixHash(rs.public_key).
|
||||
decRS, err := h.ss.decryptAndHash(currPK.Public, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h.rs = decRS
|
||||
} else {
|
||||
return nil, errors.New("noise read s, incorrect encryption flag for public key")
|
||||
}
|
||||
|
||||
// We delete processed public key
|
||||
inHandshakeMessage = inHandshakeMessage[1:]
|
||||
|
||||
// If user is writing the "s" token
|
||||
} else if writing {
|
||||
// If the local static key is not set (the handshake state was not properly initialized), we raise an error
|
||||
if h.s.IsDefault() {
|
||||
return nil, errors.New("static key not set")
|
||||
}
|
||||
|
||||
// We encrypt the public part of the static key in case a key is set in the Cipher State
|
||||
// That is, encS may either be an encrypted or unencrypted static key.
|
||||
encS, err := h.ss.encryptAndHash(h.s.Public, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// We add the (encrypted) static public key to the Waku payload
|
||||
// Note that encS = (Enc(s) || tag) if encryption key is set, otherwise encS = s.
|
||||
// We distinguish these two cases by checking length of encryption and we set the proper encryption flag
|
||||
if len(encS) > h.handshakePattern.dhKey.DHLen() {
|
||||
outHandshakeMessage = append(outHandshakeMessage, byteToNoisePublicKey(h.handshakePattern.dhKey, encS))
|
||||
} else {
|
||||
outHandshakeMessage = append(outHandshakeMessage, byteToNoisePublicKey(h.handshakePattern.dhKey, encS))
|
||||
}
|
||||
}
|
||||
case PSK:
|
||||
// If user is reading the "psk" token
|
||||
|
||||
// Calls MixKeyAndHash(psk)
|
||||
h.ss.mixKeyAndHash(h.psk)
|
||||
case EE:
|
||||
// If user is reading the "ee" token
|
||||
|
||||
// If local and/or remote ephemeral keys are not set, we raise an error
|
||||
if h.e.IsDefault() || len(h.re) == 0 {
|
||||
return nil, errors.New("local or remote ephemeral key not set")
|
||||
}
|
||||
|
||||
// Calls MixKey(DH(e, re)).
|
||||
k, err := h.handshakePattern.dhKey.DH(h.e.Private, h.re)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h.ss.mixKey(k)
|
||||
case ES:
|
||||
// If user is reading the "es" token
|
||||
|
||||
// We check if keys are correctly set.
|
||||
// If both present, we call MixKey(DH(e, rs)) if initiator, MixKey(DH(s, re)) if responder.
|
||||
if h.initiator {
|
||||
if h.e.IsDefault() || len(h.rs) == 0 {
|
||||
return nil, errors.New("local or remote ephemeral/static key not set")
|
||||
}
|
||||
|
||||
k, err := h.handshakePattern.dhKey.DH(h.e.Private, h.rs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h.ss.mixKey(k)
|
||||
} else {
|
||||
if len(h.re) == 0 || h.s.IsDefault() {
|
||||
return nil, errors.New("local or remote ephemeral/static key not set")
|
||||
}
|
||||
|
||||
k, err := h.handshakePattern.dhKey.DH(h.s.Private, h.re)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h.ss.mixKey(k)
|
||||
}
|
||||
case SE:
|
||||
// If user is reading the "se" token
|
||||
|
||||
// We check if keys are correctly set.
|
||||
// If both present, call MixKey(DH(s, re)) if initiator, MixKey(DH(e, rs)) if responder.
|
||||
if h.initiator {
|
||||
if h.s.IsDefault() || len(h.re) == 0 {
|
||||
return nil, errors.New("local or remote ephemeral/static key not set")
|
||||
}
|
||||
|
||||
k, err := h.handshakePattern.dhKey.DH(h.s.Private, h.re)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h.ss.mixKey(k)
|
||||
} else {
|
||||
if len(h.rs) == 0 || h.e.IsDefault() {
|
||||
return nil, errors.New("local or remote ephemeral/static key not set")
|
||||
}
|
||||
|
||||
k, err := h.handshakePattern.dhKey.DH(h.e.Private, h.rs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h.ss.mixKey(k)
|
||||
}
|
||||
case SS:
|
||||
// If user is reading the "ss" token
|
||||
|
||||
// If local and/or remote static keys are not set, we raise an error
|
||||
if h.s.IsDefault() || len(h.rs) == 0 {
|
||||
return nil, errors.New("local or remote static key not set")
|
||||
}
|
||||
|
||||
// Calls MixKey(DH(s, rs)).
|
||||
k, err := h.handshakePattern.dhKey.DH(h.s.Private, h.rs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h.ss.mixKey(k)
|
||||
}
|
||||
}
|
||||
|
||||
return outHandshakeMessage, nil
|
||||
}
|
||||
137
messagenametag.go
Normal file
137
messagenametag.go
Normal file
@ -0,0 +1,137 @@
|
||||
package noise
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type MessageNametag [MessageNametagLength]byte
|
||||
|
||||
const MessageNametagLength = 16
|
||||
const MessageNametagBufferSize = 50
|
||||
|
||||
var (
|
||||
ErrNametagNotFound = errors.New("message nametag not found in buffer")
|
||||
ErrNametagNotExpected = errors.New("message nametag is present in buffer but is not the next expected nametag. One or more messages were probably lost")
|
||||
)
|
||||
|
||||
// Converts a sequence or array (arbitrary size) to a MessageNametag
|
||||
func BytesToMessageNametag(input []byte) MessageNametag {
|
||||
var result MessageNametag
|
||||
copy(result[:], input)
|
||||
return result
|
||||
}
|
||||
|
||||
func (t MessageNametag) String() string {
|
||||
return hex.EncodeToString(t[:])
|
||||
}
|
||||
|
||||
type MessageNametagBuffer struct {
|
||||
buffer []MessageNametag
|
||||
counter uint64
|
||||
secret []byte
|
||||
}
|
||||
|
||||
func NewMessageNametagBuffer(secret []byte) *MessageNametagBuffer {
|
||||
return &MessageNametagBuffer{
|
||||
secret: secret,
|
||||
}
|
||||
}
|
||||
|
||||
// Initializes the empty Message nametag buffer. The n-th nametag is equal to HKDF( secret || n )
|
||||
func (m *MessageNametagBuffer) Init() {
|
||||
// We default the counter and buffer fields
|
||||
m.counter = 0
|
||||
m.buffer = make([]MessageNametag, MessageNametagBufferSize)
|
||||
if len(m.secret) != 0 {
|
||||
for i := range m.buffer {
|
||||
counterBytesLE := make([]byte, 8)
|
||||
binary.LittleEndian.PutUint64(counterBytesLE, m.counter)
|
||||
toHash := []byte{}
|
||||
toHash = append(toHash, m.secret...)
|
||||
toHash = append(toHash, counterBytesLE...)
|
||||
d := sha256.Sum256(toHash)
|
||||
m.buffer[i] = BytesToMessageNametag(d[:])
|
||||
m.counter++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MessageNametagBuffer) Pop() MessageNametag {
|
||||
// Note that if the input MessageNametagBuffer is set to default, an all 0 messageNametag is returned
|
||||
if len(m.buffer) == 0 {
|
||||
var m MessageNametag
|
||||
return m
|
||||
} else {
|
||||
messageNametag := m.buffer[0]
|
||||
m.Delete(1)
|
||||
return messageNametag
|
||||
}
|
||||
}
|
||||
|
||||
// Checks if the input messageNametag is contained in the input MessageNametagBuffer
|
||||
func (m *MessageNametagBuffer) CheckNametag(messageNametag MessageNametag) error {
|
||||
if len(m.buffer) != MessageNametagBufferSize {
|
||||
return nil
|
||||
}
|
||||
|
||||
index := -1
|
||||
for i, x := range m.buffer {
|
||||
if bytes.Equal(x[:], messageNametag[:]) {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if index == -1 {
|
||||
return ErrNametagNotFound
|
||||
} else if index > 0 {
|
||||
return ErrNametagNotExpected
|
||||
}
|
||||
|
||||
// index is 0, hence the read message tag is the next expected one
|
||||
return nil
|
||||
}
|
||||
|
||||
func rotateLeft(elems []MessageNametag, k int) []MessageNametag {
|
||||
if k < 0 || len(elems) == 0 {
|
||||
return elems
|
||||
}
|
||||
r := len(elems) - k%len(elems)
|
||||
|
||||
result := elems[r:]
|
||||
result = append(result, elems[:r]...)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Deletes the first n elements in buffer and appends n new ones
|
||||
func (m *MessageNametagBuffer) Delete(n int) {
|
||||
if n <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// We ensure n is at most MessageNametagBufferSize (the buffer will be fully replaced)
|
||||
if n > MessageNametagBufferSize {
|
||||
n = MessageNametagBufferSize
|
||||
}
|
||||
|
||||
// We update the last n values in the array if a secret is set
|
||||
// Note that if the input MessageNametagBuffer is set to default, nothing is done here
|
||||
if len(m.secret) != 0 {
|
||||
m.buffer = rotateLeft(m.buffer, n)
|
||||
for i := 0; i < n; i++ {
|
||||
counterBytesLE := make([]byte, 8)
|
||||
binary.LittleEndian.PutUint64(counterBytesLE, m.counter)
|
||||
toHash := []byte{}
|
||||
toHash = append(toHash, m.secret...)
|
||||
toHash = append(toHash, counterBytesLE...)
|
||||
d := sha256.Sum256(toHash)
|
||||
m.buffer[len(m.buffer)-n+i] = BytesToMessageNametag(d[:])
|
||||
m.counter++
|
||||
}
|
||||
}
|
||||
}
|
||||
279
noise.go
Normal file
279
noise.go
Normal file
@ -0,0 +1,279 @@
|
||||
package noise
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/cipher"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"math"
|
||||
)
|
||||
|
||||
// Waku Noise Protocols for Waku Payload Encryption
|
||||
// Noise module implementing the Noise State Objects and ChaChaPoly encryption/decryption primitives
|
||||
// See spec for more details:
|
||||
// https://github.com/vacp2p/rfc/tree/master/content/docs/rfcs/35
|
||||
//
|
||||
// Implementation partially inspired by noise-libp2p and js-libp2p-noise
|
||||
// https://github.com/status-im/nim-libp2p/blob/master/libp2p/protocols/secure/noise.nim
|
||||
// https://github.com/ChainSafe/js-libp2p-noise
|
||||
|
||||
/*
|
||||
# Noise state machine primitives
|
||||
|
||||
# Overview :
|
||||
# - Alice and Bob process (i.e. read and write, based on their role) each token appearing in a handshake pattern, consisting of pre-message and message patterns;
|
||||
# - Both users initialize and update according to processed tokens a Handshake State, a Symmetric State and a Cipher State;
|
||||
# - A preshared key psk is processed by calling MixKeyAndHash(psk);
|
||||
# - When an ephemeral public key e is read or written, the handshake hash value h is updated by calling mixHash(e); If the handshake expects a psk, MixKey(e) is further called
|
||||
# - When an encrypted static public key s or a payload message m is read, it is decrypted with decryptAndHash;
|
||||
# - When a static public key s or a payload message is written, it is encrypted with encryptAndHash;
|
||||
# - When any Diffie-Hellman token ee, es, se, ss is read or written, the chaining key ck is updated by calling MixKey on the computed secret;
|
||||
# - If all tokens are processed, users compute two new Cipher States by calling Split;
|
||||
# - The two Cipher States obtained from Split are used to encrypt/decrypt outbound/inbound messages.
|
||||
|
||||
#################################
|
||||
# Cipher State Primitives
|
||||
#################################
|
||||
*/
|
||||
|
||||
const nonceMax = math.MaxUint64 - 1 // max is reserved
|
||||
|
||||
func isEmptyKey(k []byte) bool {
|
||||
return len(k) == 0
|
||||
}
|
||||
|
||||
// The Cipher State as in https://noiseprotocol.org/noise.html#the-cipherstate-object
|
||||
// Contains an encryption key k and a nonce n (used in Noise as a counter)
|
||||
type CipherState struct {
|
||||
k []byte
|
||||
n uint64
|
||||
cipherFn func([]byte) (cipher.AEAD, error)
|
||||
}
|
||||
|
||||
func NewCipherState(k []byte, cipherFn func([]byte) (cipher.AEAD, error)) *CipherState {
|
||||
return &CipherState{
|
||||
k: k,
|
||||
cipherFn: cipherFn,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CipherState) Equals(b *CipherState) bool {
|
||||
return bytes.Equal(c.k[:], b.k[:]) && c.n == b.n
|
||||
}
|
||||
|
||||
// Checks if a Cipher State has an encryption key set
|
||||
func (c *CipherState) hasKey() bool {
|
||||
return !isEmptyKey(c.k)
|
||||
}
|
||||
|
||||
func (cs *CipherState) nonce() []byte {
|
||||
var nonceBytes [12]byte // RFC7539 specifies 12 bytes for nonce.
|
||||
binary.BigEndian.PutUint64(nonceBytes[4:], cs.n)
|
||||
return nonceBytes[:]
|
||||
}
|
||||
|
||||
// Encrypts a plaintext using key material in a Noise Cipher State
|
||||
// The CipherState is updated increasing the nonce (used as a counter in Noise) by one
|
||||
func (cs *CipherState) encryptWithAd(ad []byte, plaintext []byte) ([]byte, error) {
|
||||
// We raise an error if encryption is called using a Cipher State with nonce greater than MaxNonce
|
||||
if cs.n > nonceMax {
|
||||
return nil, errors.New("noise max nonce value reached")
|
||||
}
|
||||
|
||||
var ciphertext []byte
|
||||
|
||||
if cs.hasKey() {
|
||||
c, err := cs.cipherFn(cs.k)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// If an encryption key is set in the Cipher state, we proceed with encryption
|
||||
ciphertext = c.Seal(nil, cs.nonce(), plaintext, ad)
|
||||
|
||||
// We increase the Cipher state nonce
|
||||
cs.n++
|
||||
|
||||
// If the nonce is greater than the maximum allowed nonce, we raise an exception
|
||||
if cs.n > nonceMax {
|
||||
return nil, errors.New("noise max nonce value reached")
|
||||
}
|
||||
} else {
|
||||
// Otherwise we return the input plaintext according to specification http://www.noiseprotocol.org/noise.html#the-cipherstate-object
|
||||
ciphertext = plaintext
|
||||
}
|
||||
|
||||
return ciphertext, nil
|
||||
}
|
||||
|
||||
// Decrypts a ciphertext using key material in a Noise Cipher State
|
||||
// The CipherState is updated increasing the nonce (used as a counter in Noise) by one
|
||||
func (cs *CipherState) decryptWithAd(ad []byte, ciphertext []byte) ([]byte, error) {
|
||||
// We raise an error if encryption is called using a Cipher State with nonce greater than MaxNonce
|
||||
if cs.n > nonceMax {
|
||||
return nil, errors.New("noise max nonce value reached")
|
||||
}
|
||||
if cs.hasKey() {
|
||||
c, err := cs.cipherFn(cs.k)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
plaintext, err := c.Open(nil, cs.nonce(), ciphertext, ad)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We increase the Cipher state nonce
|
||||
cs.n++
|
||||
|
||||
// If the nonce is greater than the maximum allowed nonce, we raise an exception
|
||||
if cs.n > nonceMax {
|
||||
return nil, errors.New("noise max nonce value reached")
|
||||
}
|
||||
|
||||
return plaintext, nil
|
||||
} else {
|
||||
// Otherwise we return the input ciphertext according to specification
|
||||
// http://www.noiseprotocol.org/noise.html#the-cipherstate-object
|
||||
return ciphertext, nil
|
||||
}
|
||||
}
|
||||
|
||||
func hashProtocol(hsPattern HandshakePattern) []byte {
|
||||
// If protocol_name is less than or equal to HASHLEN bytes in length,
|
||||
// sets h equal to protocol_name with zero bytes appended to make HASHLEN bytes.
|
||||
// Otherwise sets h = HASH(protocol_name).
|
||||
protocolName := []byte(hsPattern.name)
|
||||
|
||||
if len(protocolName) <= hsPattern.hashFn().Size() {
|
||||
result := make([]byte, hsPattern.hashFn().Size())
|
||||
copy(result, protocolName)
|
||||
return result
|
||||
} else {
|
||||
h := hsPattern.hashFn()
|
||||
h.Write([]byte(hsPattern.name))
|
||||
return h.Sum(nil)
|
||||
}
|
||||
}
|
||||
|
||||
// The Symmetric State as in https://noiseprotocol.org/noise.html#the-symmetricstate-object
|
||||
// Contains a Cipher State cs, the chaining key ck and the handshake hash value h
|
||||
type SymmetricState struct {
|
||||
cs *CipherState
|
||||
hsPattern HandshakePattern
|
||||
h []byte // handshake hash
|
||||
ck []byte // chaining key
|
||||
}
|
||||
|
||||
func NewSymmetricState(hsPattern HandshakePattern) *SymmetricState {
|
||||
h := hashProtocol(hsPattern)
|
||||
|
||||
s := &SymmetricState{
|
||||
cs: NewCipherState([]byte{}, hsPattern.cipherFn),
|
||||
hsPattern: hsPattern,
|
||||
}
|
||||
|
||||
s.h = make([]byte, len(h))
|
||||
copy(s.h, h)
|
||||
|
||||
s.ck = make([]byte, len(h))
|
||||
copy(s.ck, h)
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *SymmetricState) Equals(b *SymmetricState) bool {
|
||||
return b.cs.Equals(s.cs) && bytes.Equal(s.ck, b.ck) && bytes.Equal(s.h, b.h) && s.hsPattern.Equals(b.hsPattern)
|
||||
}
|
||||
|
||||
// MixKey as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
|
||||
// Updates a Symmetric state chaining key and symmetric state
|
||||
func (s *SymmetricState) mixKey(inputKeyMaterial []byte) {
|
||||
// We derive two keys using HKDF
|
||||
keyLen := s.hsPattern.hashFn().Size()
|
||||
output := getHKDF(s.hsPattern.hashFn, s.ck, inputKeyMaterial, keyLen*2)
|
||||
ck := output[:keyLen]
|
||||
tempK := output[keyLen:]
|
||||
// We update ck and the Cipher state's key k using the output of HDKF
|
||||
s.cs = NewCipherState(tempK, s.hsPattern.cipherFn)
|
||||
s.ck = ck
|
||||
}
|
||||
|
||||
// MixHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
|
||||
// Hashes data into a Symmetric State's handshake hash value h
|
||||
func (s *SymmetricState) mixHash(data []byte) {
|
||||
// We hash the previous handshake hash and input data and store the result in the Symmetric State's handshake hash value
|
||||
h := s.hsPattern.hashFn()
|
||||
h.Write(s.h[:])
|
||||
h.Write(data)
|
||||
s.h = h.Sum(nil)
|
||||
}
|
||||
|
||||
// mixKeyAndHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
|
||||
// Combines MixKey and MixHash
|
||||
func (s *SymmetricState) mixKeyAndHash(inputKeyMaterial []byte) {
|
||||
// Derives 3 keys using HKDF, the chaining key and the input key material
|
||||
keyLen := s.hsPattern.hashFn().Size()
|
||||
output := getHKDF(s.hsPattern.hashFn, s.ck, inputKeyMaterial, keyLen*3)
|
||||
tmpKey0 := output[:keyLen]
|
||||
tmpKey1 := output[keyLen : keyLen*2]
|
||||
tmpKey2 := output[keyLen*2:]
|
||||
|
||||
// Sets the chaining key
|
||||
s.ck = tmpKey0
|
||||
// Updates the handshake hash value
|
||||
s.mixHash(tmpKey1)
|
||||
// Updates the Cipher state's key
|
||||
// Note for later support of 512 bits hash functions: "If HASHLEN is 64, then truncates tempKeys[2] to 32 bytes."
|
||||
s.cs = NewCipherState(tmpKey2, s.hsPattern.cipherFn)
|
||||
}
|
||||
|
||||
// EncryptAndHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
|
||||
// Combines encryptWithAd and mixHash
|
||||
// Note that by setting extraAd, it is possible to pass extra additional data that will be concatenated to the ad specified by Noise (can be used to authenticate messageNametag)
|
||||
func (s *SymmetricState) encryptAndHash(plaintext []byte, extraAd []byte) ([]byte, error) {
|
||||
// The additional data
|
||||
ad := append([]byte(nil), s.h[:]...)
|
||||
ad = append(ad, extraAd...)
|
||||
// Note that if an encryption key is not set yet in the Cipher state, ciphertext will be equal to plaintext
|
||||
ciphertext, err := s.cs.encryptWithAd(ad, plaintext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// We call mixHash over the result
|
||||
s.mixHash(ciphertext)
|
||||
|
||||
return ciphertext, nil
|
||||
}
|
||||
|
||||
// DecryptAndHash as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
|
||||
// Combines decryptWithAd and mixHash
|
||||
func (s *SymmetricState) decryptAndHash(ciphertext []byte, extraAd []byte) ([]byte, error) {
|
||||
// The additional data
|
||||
ad := append([]byte(nil), s.h[:]...)
|
||||
ad = append(ad, extraAd...)
|
||||
|
||||
// Note that if an encryption key is not set yet in the Cipher state, plaintext will be equal to ciphertext
|
||||
plaintext, err := s.cs.decryptWithAd(ad, ciphertext)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// According to specification, the ciphertext enters mixHash (and not the plaintext)
|
||||
s.mixHash(ciphertext)
|
||||
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
// Split as per Noise specification http://www.noiseprotocol.org/noise.html#the-symmetricstate-object
|
||||
// Once a handshake is complete, returns two Cipher States to encrypt/decrypt outbound/inbound messages
|
||||
func (s *SymmetricState) split() (*CipherState, *CipherState) {
|
||||
// Derives 2 keys using HKDF and the chaining key
|
||||
keyLen := s.hsPattern.hashFn().Size()
|
||||
output := getHKDF(s.hsPattern.hashFn, s.ck, []byte{}, keyLen*2)
|
||||
tmpKey1 := output[:keyLen]
|
||||
tmpKey2 := output[keyLen:]
|
||||
// Returns a tuple of two Cipher States initialized with the derived keys
|
||||
return NewCipherState(tmpKey1, s.hsPattern.cipherFn), NewCipherState(tmpKey2, s.hsPattern.cipherFn)
|
||||
}
|
||||
215
noise_test.go
Normal file
215
noise_test.go
Normal file
@ -0,0 +1,215 @@
|
||||
package noise
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func generateRandomBytes(t *testing.T, n int) []byte {
|
||||
b := make([]byte, n)
|
||||
_, err := rand.Read(b)
|
||||
require.NoError(t, err)
|
||||
return b
|
||||
}
|
||||
|
||||
func TestSerialization(t *testing.T) {
|
||||
handshakeMessages := make([]*NoisePublicKey, 2)
|
||||
|
||||
pk1, _ := DH25519.GenerateKeypair()
|
||||
|
||||
pk2, _ := DH25519.GenerateKeypair()
|
||||
|
||||
handshakeMessages[0] = byteToNoisePublicKey(DH25519, pk1.Public)
|
||||
handshakeMessages[1] = byteToNoisePublicKey(DH25519, pk2.Public)
|
||||
|
||||
p1 := &PayloadV2{
|
||||
ProtocolId: Noise_K1K1_25519_ChaChaPoly_SHA256,
|
||||
HandshakeMessage: handshakeMessages,
|
||||
TransportMessage: []byte{9, 8, 7, 6, 5, 4, 3, 2, 1},
|
||||
}
|
||||
|
||||
serializedPayload, err := p1.Serialize()
|
||||
require.NoError(t, err)
|
||||
|
||||
deserializedPayload, err := DeserializePayloadV2(serializedPayload)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, p1, deserializedPayload)
|
||||
}
|
||||
|
||||
func handshakeTest(t *testing.T, hsAlice *Handshake, hsBob *Handshake) {
|
||||
// ###############
|
||||
// # 1st step
|
||||
// ###############
|
||||
|
||||
// By being the handshake initiator, Alice writes a Waku2 payload v2 containing her handshake message
|
||||
// and the (encrypted) transport message
|
||||
sentTransportMessage := generateRandomBytes(t, 32)
|
||||
aliceStep, err := hsAlice.Step(nil, sentTransportMessage, MessageNametag{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Bob reads Alice's payloads, and returns the (decrypted) transport message Alice sent to him
|
||||
bobStep, err := hsBob.Step(aliceStep.PayloadV2, nil, MessageNametag{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// check:
|
||||
require.Equal(t, sentTransportMessage, bobStep.TransportMessage)
|
||||
|
||||
// ###############
|
||||
// # 2nd step
|
||||
// ###############
|
||||
|
||||
// At this step, Bob writes and returns a payload
|
||||
sentTransportMessage = generateRandomBytes(t, 32)
|
||||
bobStep, err = hsBob.Step(nil, sentTransportMessage, MessageNametag{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// While Alice reads and returns the (decrypted) transport message
|
||||
aliceStep, err = hsAlice.Step(bobStep.PayloadV2, nil, MessageNametag{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// check:
|
||||
require.Equal(t, sentTransportMessage, aliceStep.TransportMessage)
|
||||
|
||||
// ###############
|
||||
// # 3rd step
|
||||
// ###############
|
||||
|
||||
// Similarly as in first step, Alice writes a Waku2 payload containing the handshake message and the (encrypted) transport message
|
||||
sentTransportMessage = generateRandomBytes(t, 32)
|
||||
aliceStep, err = hsAlice.Step(nil, sentTransportMessage, MessageNametag{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Bob reads Alice's payloads, and returns the (decrypted) transport message Alice sent to him
|
||||
bobStep, err = hsBob.Step(aliceStep.PayloadV2, nil, MessageNametag{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// check:
|
||||
require.Equal(t, sentTransportMessage, bobStep.TransportMessage)
|
||||
|
||||
_, err = hsAlice.FinalizeHandshake()
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = hsBob.FinalizeHandshake()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Note that for this handshake pattern, no more message patterns are left for processing
|
||||
// We test that extra calls to stepHandshake do not affect parties' handshake states
|
||||
require.True(t, hsAlice.IsComplete())
|
||||
require.True(t, hsBob.IsComplete())
|
||||
|
||||
_, err = hsAlice.Step(nil, generateRandomBytes(t, 32), MessageNametag{})
|
||||
require.ErrorIs(t, err, ErrHandshakeComplete)
|
||||
|
||||
_, err = hsBob.Step(nil, generateRandomBytes(t, 32), MessageNametag{})
|
||||
require.ErrorIs(t, err, ErrHandshakeComplete)
|
||||
|
||||
// #########################
|
||||
// After Handshake
|
||||
// #########################
|
||||
|
||||
// We test read/write of random messages exchanged between Alice and Bob
|
||||
|
||||
defaultMessageNametagBuffer := NewMessageNametagBuffer(nil)
|
||||
|
||||
aliceHSResult, err := hsAlice.FinalizeHandshake()
|
||||
require.NoError(t, err)
|
||||
|
||||
bobHSResult, err := hsBob.FinalizeHandshake()
|
||||
require.NoError(t, err)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
// Alice writes to Bob
|
||||
message := generateRandomBytes(t, 32)
|
||||
|
||||
encryptedPayload, err := aliceHSResult.WriteMessage(message, defaultMessageNametagBuffer)
|
||||
require.NoError(t, err)
|
||||
|
||||
plaintext, err := bobHSResult.ReadMessage(encryptedPayload, defaultMessageNametagBuffer)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, message, plaintext)
|
||||
|
||||
// Bob writes to Alice
|
||||
message = generateRandomBytes(t, 32)
|
||||
|
||||
encryptedPayload, err = bobHSResult.WriteMessage(message, defaultMessageNametagBuffer)
|
||||
require.NoError(t, err)
|
||||
|
||||
plaintext, err = aliceHSResult.ReadMessage(encryptedPayload, defaultMessageNametagBuffer)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, message, plaintext)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoiseXXHandshakeRoundtrip(t *testing.T) {
|
||||
aliceKP, _ := DH25519.GenerateKeypair()
|
||||
bobKP, _ := DH25519.GenerateKeypair()
|
||||
|
||||
hsAlice, err := NewHandshake_XX_25519_ChaChaPoly_SHA256(aliceKP, true, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
hsBob, err := NewHandshake_XX_25519_ChaChaPoly_SHA256(bobKP, false, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
handshakeTest(t, hsAlice, hsBob)
|
||||
}
|
||||
|
||||
func TestNoiseXXpsk0HandshakeRoundtrip(t *testing.T) {
|
||||
aliceKP, _ := DH25519.GenerateKeypair()
|
||||
bobKP, _ := DH25519.GenerateKeypair()
|
||||
|
||||
// We generate a random psk
|
||||
psk := generateRandomBytes(t, 32)
|
||||
|
||||
hsAlice, err := NewHandshake_XXpsk0_25519_ChaChaPoly_SHA256(aliceKP, true, psk, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
hsBob, err := NewHandshake_XXpsk0_25519_ChaChaPoly_SHA256(bobKP, false, psk, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
handshakeTest(t, hsAlice, hsBob)
|
||||
}
|
||||
|
||||
func TestNoiseK1K1HandshakeRoundtrip(t *testing.T) {
|
||||
aliceKP, _ := DH25519.GenerateKeypair()
|
||||
bobKP, _ := DH25519.GenerateKeypair()
|
||||
|
||||
hsAlice, err := NewHandshake_K1K1_25519_ChaChaPoly_SHA256(aliceKP, true, bobKP.Public, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
hsBob, err := NewHandshake_K1K1_25519_ChaChaPoly_SHA256(bobKP, false, aliceKP.Public, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
handshakeTest(t, hsAlice, hsBob)
|
||||
}
|
||||
|
||||
func TestNoiseXK1HandshakeRoundtrip(t *testing.T) {
|
||||
aliceKP, _ := DH25519.GenerateKeypair()
|
||||
bobKP, _ := DH25519.GenerateKeypair()
|
||||
|
||||
hsAlice, err := NewHandshake_XK1_25519_ChaChaPoly_SHA256(aliceKP, true, bobKP.Public, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
hsBob, err := NewHandshake_XK1_25519_ChaChaPoly_SHA256(bobKP, false, bobKP.Public, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
handshakeTest(t, hsAlice, hsBob)
|
||||
}
|
||||
|
||||
func TestPKCSPaddingUnpadding(t *testing.T) {
|
||||
maxMessageLength := 3 * NoisePaddingBlockSize
|
||||
for messageLen := 0; messageLen <= maxMessageLength; messageLen++ {
|
||||
message := generateRandomBytes(t, messageLen)
|
||||
padded, err := PKCS7_Pad(message, NoisePaddingBlockSize)
|
||||
require.NoError(t, err)
|
||||
unpadded, err := PKCS7_Unpad(padded, NoisePaddingBlockSize)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Greater(t, len(padded), 0)
|
||||
require.Equal(t, len(padded)%NoisePaddingBlockSize, 0)
|
||||
require.Equal(t, message, unpadded)
|
||||
}
|
||||
}
|
||||
48
padding.go
Normal file
48
padding.go
Normal file
@ -0,0 +1,48 @@
|
||||
package noise
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
// PKCS7_Pad pads a payload according to PKCS#7 as per
|
||||
// RFC 5652 https://datatracker.ietf.org/doc/html/rfc5652#section-6.3
|
||||
func PKCS7_Pad(payload []byte, paddingSize int) ([]byte, error) {
|
||||
if paddingSize >= 256 {
|
||||
return nil, errors.New("invalid padding size")
|
||||
}
|
||||
|
||||
k := paddingSize - (len(payload) % paddingSize)
|
||||
|
||||
var padVal int
|
||||
if k != 0 {
|
||||
padVal = k
|
||||
} else {
|
||||
padVal = paddingSize
|
||||
}
|
||||
|
||||
padding := make([]byte, padVal)
|
||||
for i := range padding {
|
||||
padding[i] = byte(padVal)
|
||||
}
|
||||
|
||||
return append(payload, padding...), nil
|
||||
}
|
||||
|
||||
// PKCS7_Unpad unpads a payload according to PKCS#7 as per
|
||||
// RFC 5652 https://datatracker.ietf.org/doc/html/rfc5652#section-6.3
|
||||
func PKCS7_Unpad(payload []byte, paddingSize int) ([]byte, error) {
|
||||
if paddingSize >= 256 {
|
||||
return nil, errors.New("invalid padding size")
|
||||
}
|
||||
|
||||
if len(payload) == 0 {
|
||||
return nil, nil // empty payload
|
||||
}
|
||||
|
||||
high := len(payload) - 1
|
||||
k := payload[high]
|
||||
|
||||
unpadded := payload[0:(high + 1 - int(k))]
|
||||
|
||||
return unpadded, nil
|
||||
}
|
||||
312
patterns.go
Normal file
312
patterns.go
Normal file
@ -0,0 +1,312 @@
|
||||
package noise
|
||||
|
||||
import (
|
||||
"crypto/cipher"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"hash"
|
||||
|
||||
"golang.org/x/crypto/chacha20poly1305"
|
||||
)
|
||||
|
||||
// The Noise tokens appearing in Noise (pre)message patterns
|
||||
// as in http://www.noiseprotocol.org/noise.html#handshake-pattern-basics
|
||||
type NoiseTokens string
|
||||
|
||||
const (
|
||||
E NoiseTokens = "e"
|
||||
S NoiseTokens = "s"
|
||||
ES NoiseTokens = "es"
|
||||
EE NoiseTokens = "ee"
|
||||
SE NoiseTokens = "se"
|
||||
SS NoiseTokens = "ss"
|
||||
PSK NoiseTokens = "psk"
|
||||
)
|
||||
|
||||
// The direction of a (pre)message pattern in canonical form (i.e. Alice-initiated form)
|
||||
// as in http://www.noiseprotocol.org/noise.html#alice-and-bob
|
||||
type MessageDirection string
|
||||
|
||||
const (
|
||||
Right MessageDirection = "->"
|
||||
Left MessageDirection = "<-"
|
||||
)
|
||||
|
||||
// The pre message pattern consisting of a message direction and some Noise tokens, if any.
|
||||
// (if non empty, only tokens e and s are allowed: http://www.noiseprotocol.org/noise.html#handshake-pattern-basics)
|
||||
type PreMessagePattern struct {
|
||||
direction MessageDirection
|
||||
tokens []NoiseTokens
|
||||
}
|
||||
|
||||
func NewPreMessagePattern(direction MessageDirection, tokens []NoiseTokens) PreMessagePattern {
|
||||
return PreMessagePattern{
|
||||
direction: direction,
|
||||
tokens: tokens,
|
||||
}
|
||||
}
|
||||
|
||||
func (p PreMessagePattern) Equals(b PreMessagePattern) bool {
|
||||
if p.direction != b.direction {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(p.tokens) != len(b.tokens) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := range p.tokens {
|
||||
if p.tokens[i] != b.tokens[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// The message pattern consisting of a message direction and some Noise tokens
|
||||
// All Noise tokens are allowed
|
||||
type MessagePattern struct {
|
||||
direction MessageDirection
|
||||
tokens []NoiseTokens
|
||||
}
|
||||
|
||||
func NewMessagePattern(direction MessageDirection, tokens []NoiseTokens) MessagePattern {
|
||||
return MessagePattern{
|
||||
direction: direction,
|
||||
tokens: tokens,
|
||||
}
|
||||
}
|
||||
|
||||
func (p MessagePattern) Equals(b MessagePattern) bool {
|
||||
if p.direction != b.direction {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(p.tokens) != len(b.tokens) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := range p.tokens {
|
||||
if p.tokens[i] != b.tokens[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// The handshake pattern object. It stores the handshake protocol name, the handshake pre message patterns and the handshake message patterns
|
||||
type HandshakePattern struct {
|
||||
protocolID byte
|
||||
name string
|
||||
premessagePatterns []PreMessagePattern
|
||||
messagePatterns []MessagePattern
|
||||
hashFn func() hash.Hash
|
||||
cipherFn func([]byte) (cipher.AEAD, error)
|
||||
dhKey DHKey
|
||||
}
|
||||
|
||||
func NewHandshakePattern(protocolID byte, name string, hashFn func() hash.Hash, cipherFn func([]byte) (cipher.AEAD, error), dhKey DHKey, preMessagePatterns []PreMessagePattern, messagePatterns []MessagePattern) HandshakePattern {
|
||||
return HandshakePattern{
|
||||
protocolID: protocolID,
|
||||
name: name,
|
||||
hashFn: hashFn,
|
||||
cipherFn: cipherFn,
|
||||
dhKey: dhKey,
|
||||
premessagePatterns: preMessagePatterns,
|
||||
messagePatterns: messagePatterns,
|
||||
}
|
||||
}
|
||||
|
||||
func (p HandshakePattern) Equals(b HandshakePattern) bool {
|
||||
|
||||
if len(p.premessagePatterns) != len(b.premessagePatterns) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := range p.premessagePatterns {
|
||||
if !p.premessagePatterns[i].Equals(b.premessagePatterns[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if len(p.messagePatterns) != len(b.messagePatterns) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := range p.messagePatterns {
|
||||
if !p.messagePatterns[i].Equals(b.messagePatterns[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return p.name == b.name
|
||||
}
|
||||
|
||||
var EmptyPreMessage = []PreMessagePattern{}
|
||||
|
||||
// Supported Noise handshake patterns as defined in https://rfc.vac.dev/spec/35/#specification
|
||||
|
||||
var K1K1 = NewHandshakePattern(
|
||||
Noise_K1K1_25519_ChaChaPoly_SHA256,
|
||||
"Noise_K1K1_25519_ChaChaPoly_SHA256",
|
||||
sha256.New,
|
||||
chacha20poly1305.New,
|
||||
DH25519,
|
||||
[]PreMessagePattern{
|
||||
NewPreMessagePattern(Right, []NoiseTokens{S}),
|
||||
NewPreMessagePattern(Left, []NoiseTokens{S}),
|
||||
},
|
||||
[]MessagePattern{
|
||||
NewMessagePattern(Right, []NoiseTokens{E}),
|
||||
NewMessagePattern(Left, []NoiseTokens{E, EE, ES}),
|
||||
NewMessagePattern(Right, []NoiseTokens{SE}),
|
||||
},
|
||||
)
|
||||
|
||||
var XK1 = NewHandshakePattern(
|
||||
Noise_XK1_25519_ChaChaPoly_SHA256,
|
||||
"Noise_XK1_25519_ChaChaPoly_SHA256",
|
||||
sha256.New,
|
||||
chacha20poly1305.New,
|
||||
DH25519,
|
||||
[]PreMessagePattern{
|
||||
NewPreMessagePattern(Left, []NoiseTokens{S}),
|
||||
},
|
||||
[]MessagePattern{
|
||||
NewMessagePattern(Right, []NoiseTokens{E}),
|
||||
NewMessagePattern(Left, []NoiseTokens{E, EE, ES}),
|
||||
NewMessagePattern(Right, []NoiseTokens{S, SE}),
|
||||
},
|
||||
)
|
||||
|
||||
var XX = NewHandshakePattern(
|
||||
Noise_XX_25519_ChaChaPoly_SHA256,
|
||||
"Noise_XX_25519_ChaChaPoly_SHA256",
|
||||
sha256.New,
|
||||
chacha20poly1305.New,
|
||||
DH25519,
|
||||
EmptyPreMessage,
|
||||
[]MessagePattern{
|
||||
NewMessagePattern(Right, []NoiseTokens{E}),
|
||||
NewMessagePattern(Left, []NoiseTokens{E, EE, S, ES}),
|
||||
NewMessagePattern(Right, []NoiseTokens{S, SE}),
|
||||
},
|
||||
)
|
||||
|
||||
var XXpsk0 = NewHandshakePattern(
|
||||
Noise_XXpsk0_25519_ChaChaPoly_SHA256,
|
||||
"Noise_XXpsk0_25519_ChaChaPoly_SHA256",
|
||||
sha256.New,
|
||||
chacha20poly1305.New,
|
||||
DH25519,
|
||||
EmptyPreMessage,
|
||||
[]MessagePattern{
|
||||
NewMessagePattern(Right, []NoiseTokens{PSK, E}),
|
||||
NewMessagePattern(Left, []NoiseTokens{E, EE, S, ES}),
|
||||
NewMessagePattern(Right, []NoiseTokens{S, SE}),
|
||||
},
|
||||
)
|
||||
|
||||
var WakuPairing = NewHandshakePattern(
|
||||
Noise_WakuPairing_25519_ChaChaPoly_SHA256,
|
||||
"Noise_WakuPairing_25519_ChaChaPoly_SHA256",
|
||||
sha256.New,
|
||||
chacha20poly1305.New,
|
||||
DH25519,
|
||||
[]PreMessagePattern{
|
||||
NewPreMessagePattern(Left, []NoiseTokens{E}),
|
||||
},
|
||||
[]MessagePattern{
|
||||
NewMessagePattern(Right, []NoiseTokens{E, EE}),
|
||||
NewMessagePattern(Left, []NoiseTokens{S, ES}),
|
||||
NewMessagePattern(Right, []NoiseTokens{S, SE, SS}),
|
||||
},
|
||||
)
|
||||
|
||||
// Supported Protocol ID for PayloadV2 objects
|
||||
// Protocol IDs are defined according to https://rfc.vac.dev/spec/35/#specification
|
||||
const Noise_K1K1_25519_ChaChaPoly_SHA256 = 10
|
||||
const Noise_XK1_25519_ChaChaPoly_SHA256 = 11
|
||||
const Noise_XX_25519_ChaChaPoly_SHA256 = 12
|
||||
const Noise_XXpsk0_25519_ChaChaPoly_SHA256 = 13
|
||||
const Noise_WakuPairing_25519_ChaChaPoly_SHA256 = 14
|
||||
const ChaChaPoly = 30
|
||||
const None = 0
|
||||
|
||||
func IsProtocolIDSupported(protocolID byte) bool {
|
||||
return protocolID == Noise_K1K1_25519_ChaChaPoly_SHA256 ||
|
||||
protocolID == Noise_XK1_25519_ChaChaPoly_SHA256 ||
|
||||
protocolID == Noise_XX_25519_ChaChaPoly_SHA256 ||
|
||||
protocolID == Noise_XXpsk0_25519_ChaChaPoly_SHA256 ||
|
||||
protocolID == ChaChaPoly ||
|
||||
protocolID == Noise_WakuPairing_25519_ChaChaPoly_SHA256 ||
|
||||
protocolID == None
|
||||
}
|
||||
|
||||
func GetHandshakePattern(protocol byte) (HandshakePattern, error) {
|
||||
switch protocol {
|
||||
case Noise_K1K1_25519_ChaChaPoly_SHA256:
|
||||
return K1K1, nil
|
||||
case Noise_XK1_25519_ChaChaPoly_SHA256:
|
||||
return XK1, nil
|
||||
case Noise_XX_25519_ChaChaPoly_SHA256:
|
||||
return XX, nil
|
||||
case Noise_XXpsk0_25519_ChaChaPoly_SHA256:
|
||||
return XXpsk0, nil
|
||||
case Noise_WakuPairing_25519_ChaChaPoly_SHA256:
|
||||
return WakuPairing, nil
|
||||
default:
|
||||
return HandshakePattern{}, errors.New("unsupported handshake pattern")
|
||||
}
|
||||
}
|
||||
|
||||
// NewHandshake_XX_25519_ChaChaPoly_SHA256 creates a handshake where the initiator and responder are not aware of each other static keys
|
||||
func NewHandshake_XX_25519_ChaChaPoly_SHA256(staticKeypair Keypair, initiator bool, prologue []byte) (*Handshake, error) {
|
||||
return NewHandshake(XX, staticKeypair, Keypair{}, prologue, nil, nil, initiator)
|
||||
}
|
||||
|
||||
// NewHandshake_XXpsk0_25519_ChaChaPoly_SHA256 creates a handshake where the initiator and responder are not aware of each other static keys
|
||||
// and use a preshared secret to strengthen their mutual authentication
|
||||
func NewHandshake_XXpsk0_25519_ChaChaPoly_SHA256(staticKeypair Keypair, initiator bool, presharedKey []byte, prologue []byte) (*Handshake, error) {
|
||||
return NewHandshake(XXpsk0, staticKeypair, Keypair{}, prologue, presharedKey, nil, initiator)
|
||||
}
|
||||
|
||||
// NewHandshake_K1K1_25519_ChaChaPoly_SHA256 creates a handshake where both initiator and recever know each other handshake. Only ephemeral keys
|
||||
// are exchanged. This handshake is useful in case the initiator needs to instantiate a new separate encrypted communication
|
||||
// channel with the responder
|
||||
func NewHandshake_K1K1_25519_ChaChaPoly_SHA256(myStaticKeypair Keypair, initiator bool, peerStaticKey []byte, prologue []byte) (*Handshake, error) {
|
||||
var presharedKeys []*NoisePublicKey
|
||||
if initiator {
|
||||
presharedKeys = append(presharedKeys, byteToNoisePublicKey(K1K1.dhKey, myStaticKeypair.Public))
|
||||
presharedKeys = append(presharedKeys, byteToNoisePublicKey(K1K1.dhKey, peerStaticKey))
|
||||
} else {
|
||||
presharedKeys = append(presharedKeys, byteToNoisePublicKey(K1K1.dhKey, peerStaticKey))
|
||||
presharedKeys = append(presharedKeys, byteToNoisePublicKey(K1K1.dhKey, myStaticKeypair.Public))
|
||||
}
|
||||
return NewHandshake(K1K1, myStaticKeypair, Keypair{}, prologue, nil, presharedKeys, initiator)
|
||||
}
|
||||
|
||||
// NewHandshake_XK1_25519_ChaChaPoly_SHA256 creates a handshake where the initiator knows the responder public static key. Within this handshake,
|
||||
// the initiator and responder reciprocally authenticate their static keys using ephemeral keys. We note that while the responder's
|
||||
// static key is assumed to be known to Alice (and hence is not transmitted), The initiator static key is sent to the
|
||||
// responder encrypted with a key derived from both parties ephemeral keys and the responder's static key.
|
||||
func NewHandshake_XK1_25519_ChaChaPoly_SHA256(myStaticKeypair Keypair, initiator bool, responderStaticKey []byte, prologue []byte) (*Handshake, error) {
|
||||
if !initiator {
|
||||
// Overwrite responderStaticKey with responder's static key in case they're different
|
||||
responderStaticKey = myStaticKeypair.Public
|
||||
}
|
||||
pubK := byteToNoisePublicKey(XK1.dhKey, responderStaticKey)
|
||||
return NewHandshake(XK1, myStaticKeypair, Keypair{}, prologue, nil, []*NoisePublicKey{pubK}, initiator)
|
||||
}
|
||||
|
||||
// NewHandshake_WakuPairing_25519_ChaChaPoly_SHA256
|
||||
func NewHandshake_WakuPairing_25519_ChaChaPoly_SHA256(myStaticKeypair Keypair, myEphemeralKeypair Keypair, initiator bool, prologue []byte, receiverEphemeralKey []byte) (*Handshake, error) {
|
||||
if !initiator {
|
||||
// Overwrite responderStaticKey with responder's static key in case they're different
|
||||
receiverEphemeralKey = myEphemeralKeypair.Public
|
||||
}
|
||||
pubK := byteToNoisePublicKey(WakuPairing.dhKey, receiverEphemeralKey)
|
||||
return NewHandshake(WakuPairing, myStaticKeypair, myEphemeralKeypair, prologue, nil, []*NoisePublicKey{pubK}, initiator)
|
||||
}
|
||||
210
payload.go
Normal file
210
payload.go
Normal file
@ -0,0 +1,210 @@
|
||||
package noise
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ed25519"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
)
|
||||
|
||||
const MaxUint8 = 1<<8 - 1
|
||||
|
||||
// PayloadV2 defines an object for Waku payloads with version 2 as in
|
||||
// https://rfc.vac.dev/spec/35/#public-keys-serialization
|
||||
// It contains a protocol ID field, the handshake message (for Noise handshakes) and
|
||||
// a transport message (for Noise handshakes and ChaChaPoly encryptions)
|
||||
type PayloadV2 struct {
|
||||
ProtocolId byte
|
||||
HandshakeMessage []*NoisePublicKey
|
||||
TransportMessage []byte
|
||||
MessageNametag MessageNametag
|
||||
}
|
||||
|
||||
// Checks equality between two PayloadsV2 objects
|
||||
func (p *PayloadV2) Equals(p2 *PayloadV2) bool {
|
||||
if p.ProtocolId != p2.ProtocolId || !bytes.Equal(p.TransportMessage, p2.TransportMessage) {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, p1 := range p.HandshakeMessage {
|
||||
for _, p2 := range p2.HandshakeMessage {
|
||||
if !p1.Equals(p2) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Serializes a PayloadV2 object to a byte sequences according to https://rfc.vac.dev/spec/35/
|
||||
// The output serialized payload concatenates the input PayloadV2 object fields as
|
||||
// payload = ( protocolId || serializedHandshakeMessageLen || serializedHandshakeMessage || transportMessageLen || transportMessage)
|
||||
// The output can be then passed to the payload field of a WakuMessage https://rfc.vac.dev/spec/14/
|
||||
func (p *PayloadV2) Serialize() ([]byte, error) {
|
||||
// We collect public keys contained in the handshake message
|
||||
|
||||
// According to https://rfc.vac.dev/spec/35/, the maximum size for the handshake message is 256 bytes, that is
|
||||
// the handshake message length can be represented with 1 byte only. (its length can be stored in 1 byte)
|
||||
// However, to ease public keys length addition operation, we declare it as int and later cast to uit8
|
||||
serializedHandshakeMessageLen := 0
|
||||
// This variables will store the concatenation of the serializations of all public keys in the handshake message
|
||||
serializedHandshakeMessage := make([]byte, 0, 256)
|
||||
serializedHandshakeMessageBuffer := bytes.NewBuffer(serializedHandshakeMessage)
|
||||
|
||||
for _, pk := range p.HandshakeMessage {
|
||||
serializedPK := pk.Serialize()
|
||||
serializedHandshakeMessageLen += len(serializedPK)
|
||||
if _, err := serializedHandshakeMessageBuffer.Write(serializedPK); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if serializedHandshakeMessageLen > MaxUint8 {
|
||||
return nil, errors.New("too many public keys in handshake message")
|
||||
}
|
||||
}
|
||||
|
||||
// The output payload as in https://rfc.vac.dev/spec/35/. We concatenate all the PayloadV2 fields as
|
||||
// payload = ( protocolId || serializedHandshakeMessageLen || serializedHandshakeMessage || transportMessageLen || transportMessage)
|
||||
|
||||
// We declare it as a byte sequence of length accordingly to the PayloadV2 information read
|
||||
payload := make([]byte, 0, MessageNametagLength+
|
||||
1+ // 1 byte for protocol ID
|
||||
1+ // 1 byte for length of serializedHandshakeMessage field
|
||||
serializedHandshakeMessageLen+ // serializedHandshakeMessageLen bytes for serializedHandshakeMessage
|
||||
8+ // 8 bytes for transportMessageLen
|
||||
len(p.TransportMessage), // transportMessageLen bytes for transportMessage
|
||||
)
|
||||
|
||||
payloadBuf := bytes.NewBuffer(payload)
|
||||
|
||||
if _, err := payloadBuf.Write(p.MessageNametag[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// The protocol ID (1 byte) and handshake message length (1 byte) can be directly casted to byte to allow direct copy to the payload byte sequence
|
||||
if err := payloadBuf.WriteByte(p.ProtocolId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := payloadBuf.WriteByte(byte(serializedHandshakeMessageLen)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := payloadBuf.Write(serializedHandshakeMessageBuffer.Bytes()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
TransportMessageLen := uint64(len(p.TransportMessage))
|
||||
if err := binary.Write(payloadBuf, binary.LittleEndian, TransportMessageLen); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, err := payloadBuf.Write(p.TransportMessage); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return payloadBuf.Bytes(), nil
|
||||
}
|
||||
|
||||
const ChaChaPolyTagSize = byte(16)
|
||||
|
||||
// Deserializes a byte sequence to a PayloadV2 object according to https://rfc.vac.dev/spec/35/.
|
||||
// The input serialized payload concatenates the output PayloadV2 object fields as
|
||||
// payload = ( protocolId || serializedHandshakeMessageLen || serializedHandshakeMessage || transportMessageLen || transportMessage)
|
||||
func DeserializePayloadV2(payload []byte) (*PayloadV2, error) {
|
||||
payloadBuf := bytes.NewBuffer(payload)
|
||||
|
||||
result := &PayloadV2{}
|
||||
|
||||
// We start by reading the messageNametag
|
||||
if err := binary.Read(payloadBuf, binary.BigEndian, &result.MessageNametag); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// We read the Protocol ID
|
||||
if err := binary.Read(payloadBuf, binary.BigEndian, &result.ProtocolId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !IsProtocolIDSupported(result.ProtocolId) {
|
||||
return nil, errors.New("unsupported protocol")
|
||||
}
|
||||
|
||||
// We read the Handshake Message length (1 byte)
|
||||
var handshakeMessageLen byte
|
||||
if err := binary.Read(payloadBuf, binary.BigEndian, &handshakeMessageLen); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if handshakeMessageLen > MaxUint8 {
|
||||
return nil, errors.New("too many public keys in handshake message")
|
||||
}
|
||||
|
||||
written := byte(0)
|
||||
var handshakeMessages []*NoisePublicKey
|
||||
for written < handshakeMessageLen {
|
||||
// We obtain the current Noise Public key encryption flag
|
||||
flag, err := payloadBuf.ReadByte()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if flag == 0 {
|
||||
// If the key is unencrypted, we only read the X coordinate of the EC public key and we deserialize into a Noise Public Key
|
||||
pkLen := ed25519.PublicKeySize
|
||||
var pkBytes SerializedNoisePublicKey = make([]byte, pkLen)
|
||||
if err := binary.Read(payloadBuf, binary.BigEndian, &pkBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
serializedPK := SerializedNoisePublicKey(make([]byte, ed25519.PublicKeySize+1))
|
||||
serializedPK[0] = flag
|
||||
copy(serializedPK[1:], pkBytes)
|
||||
|
||||
pk, err := serializedPK.Unserialize()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
handshakeMessages = append(handshakeMessages, pk)
|
||||
written += uint8(1 + pkLen)
|
||||
} else if flag == 1 {
|
||||
// If the key is encrypted, we only read the encrypted X coordinate and the authorization tag, and we deserialize into a Noise Public Key
|
||||
pkLen := ed25519.PublicKeySize + ChaChaPolyTagSize
|
||||
// TODO: duplicated code: ==============
|
||||
|
||||
var pkBytes SerializedNoisePublicKey = make([]byte, pkLen)
|
||||
if err := binary.Read(payloadBuf, binary.BigEndian, &pkBytes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
serializedPK := SerializedNoisePublicKey(make([]byte, ed25519.PublicKeySize+1))
|
||||
serializedPK[0] = flag
|
||||
copy(serializedPK[1:], pkBytes)
|
||||
|
||||
pk, err := serializedPK.Unserialize()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
handshakeMessages = append(handshakeMessages, pk)
|
||||
written += uint8(1 + pkLen)
|
||||
// TODO: duplicated
|
||||
} else {
|
||||
return nil, errors.New("invalid flag for Noise public key")
|
||||
}
|
||||
}
|
||||
|
||||
result.HandshakeMessage = handshakeMessages
|
||||
|
||||
var TransportMessageLen uint64
|
||||
if err := binary.Read(payloadBuf, binary.LittleEndian, &TransportMessageLen); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result.TransportMessage = make([]byte, TransportMessageLen)
|
||||
if err := binary.Read(payloadBuf, binary.BigEndian, &result.TransportMessage); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
102
publickey.go
Normal file
102
publickey.go
Normal file
@ -0,0 +1,102 @@
|
||||
package noise
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
)
|
||||
|
||||
// A Noise public key is a public key exchanged during Noise handshakes (no private part)
|
||||
// This follows https://rfc.vac.dev/spec/35/#public-keys-serialization
|
||||
// pk contains the X coordinate of the public key, if unencrypted (this implies flag = 0)
|
||||
// or the encryption of the X coordinate concatenated with the authorization tag, if encrypted (this implies flag = 1)
|
||||
// Note: besides encryption, flag can be used to distinguish among multiple supported Elliptic Curves
|
||||
type NoisePublicKey struct {
|
||||
Flag byte
|
||||
Public []byte
|
||||
}
|
||||
|
||||
func NewNoisePublicKey(flag byte, public []byte) *NoisePublicKey {
|
||||
return &NoisePublicKey{
|
||||
Flag: flag,
|
||||
Public: public,
|
||||
}
|
||||
}
|
||||
|
||||
func byteToNoisePublicKey(dhKey DHKey, input []byte) *NoisePublicKey {
|
||||
flag := byte(0)
|
||||
if len(input) > dhKey.DHLen() {
|
||||
flag = 1
|
||||
}
|
||||
|
||||
return &NoisePublicKey{
|
||||
Flag: flag,
|
||||
Public: input,
|
||||
}
|
||||
}
|
||||
|
||||
// Equals checks equality between two Noise public keys
|
||||
func (pk *NoisePublicKey) Equals(pk2 *NoisePublicKey) bool {
|
||||
return pk.Flag == pk2.Flag && bytes.Equal(pk.Public, pk2.Public)
|
||||
}
|
||||
|
||||
type SerializedNoisePublicKey []byte
|
||||
|
||||
// Serialize converts a Noise public key to a stream of bytes as in
|
||||
// https://rfc.vac.dev/spec/35/#public-keys-serialization
|
||||
func (pk *NoisePublicKey) Serialize() SerializedNoisePublicKey {
|
||||
// Public key is serialized as (flag || pk)
|
||||
// Note that pk contains the X coordinate of the public key if unencrypted
|
||||
// or the encryption concatenated with the authorization tag if encrypted
|
||||
serializedPK := make([]byte, len(pk.Public)+1)
|
||||
serializedPK[0] = pk.Flag
|
||||
copy(serializedPK[1:], pk.Public)
|
||||
|
||||
return serializedPK
|
||||
}
|
||||
|
||||
// Unserialize converts a serialized Noise public key to a NoisePublicKey object as in
|
||||
// https://rfc.vac.dev/spec/35/#public-keys-serialization
|
||||
func (s SerializedNoisePublicKey) Unserialize() (*NoisePublicKey, error) {
|
||||
if len(s) <= 1 {
|
||||
return nil, errors.New("invalid serialized public key length")
|
||||
}
|
||||
|
||||
pubk := &NoisePublicKey{}
|
||||
pubk.Flag = s[0]
|
||||
if !(pubk.Flag == 0 || pubk.Flag == 1) {
|
||||
return nil, errors.New("invalid flag in serialized public key")
|
||||
}
|
||||
|
||||
pubk.Public = s[1:]
|
||||
|
||||
return pubk, nil
|
||||
}
|
||||
|
||||
// Encrypt encrypts a Noise public key using a Cipher State
|
||||
func (pk *NoisePublicKey) Encrypt(state *CipherState) error {
|
||||
if pk.Flag == 0 {
|
||||
// Authorization tag is appended to output
|
||||
encPk, err := state.encryptWithAd(nil, pk.Public)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pk.Flag = 1
|
||||
pk.Public = encPk
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Decrypts decrypts a Noise public key using a Cipher State
|
||||
func (pk *NoisePublicKey) Decrypt(state *CipherState) error {
|
||||
if pk.Flag == 1 {
|
||||
decPk, err := state.decryptWithAd(nil, pk.Public) // encrypted pk should contain the auth tag
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pk.Flag = 0
|
||||
pk.Public = decPk
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
190
wakupairing_test.go
Normal file
190
wakupairing_test.go
Normal file
@ -0,0 +1,190 @@
|
||||
package noise
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestWakuPairing(t *testing.T) {
|
||||
// Pairing Phase
|
||||
// ==========
|
||||
|
||||
// Alice static/ephemeral key initialization and commitment
|
||||
aliceStaticKey, _ := DH25519.GenerateKeypair()
|
||||
aliceEphemeralKey, _ := DH25519.GenerateKeypair()
|
||||
s := generateRandomBytes(t, 32)
|
||||
aliceCommittedStaticKey := CommitPublicKey(sha256.New, aliceStaticKey.Public, s)
|
||||
|
||||
// Bob static/ephemeral key initialization and commitment
|
||||
bobStaticKey, _ := DH25519.GenerateKeypair()
|
||||
bobEphemeralKey, _ := DH25519.GenerateKeypair()
|
||||
r := generateRandomBytes(t, 32)
|
||||
bobCommittedStaticKey := CommitPublicKey(sha256.New, bobStaticKey.Public, r)
|
||||
|
||||
prologue := generateRandomBytes(t, 100)
|
||||
messageNametag := BytesToMessageNametag(generateRandomBytes(t, MessageNametagLength))
|
||||
|
||||
// We initialize the Handshake states.
|
||||
// Note that we pass the whole qr serialization as prologue information
|
||||
aliceHS, err := NewHandshake_WakuPairing_25519_ChaChaPoly_SHA256(aliceStaticKey, aliceEphemeralKey, true, prologue, bobEphemeralKey.Public)
|
||||
require.NoError(t, err)
|
||||
|
||||
bobHS, err := NewHandshake_WakuPairing_25519_ChaChaPoly_SHA256(bobStaticKey, bobEphemeralKey, false, prologue, bobEphemeralKey.Public)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Pairing Handshake
|
||||
// ==========
|
||||
|
||||
// Write and read calls alternate between Alice and Bob: the handhshake progresses by alternatively calling stepHandshake for each user
|
||||
|
||||
// 1st step
|
||||
// -> eA, eAeB {H(sA||s)} [authcode]
|
||||
|
||||
// The messageNametag for the first handshake message is randomly generated and exchanged out-of-band
|
||||
// and corresponds to qrMessageNametag
|
||||
|
||||
// We set the transport message to be H(sA||s)
|
||||
sentTransportMessage := aliceCommittedStaticKey
|
||||
|
||||
// By being the handshake initiator, Alice writes a Waku2 payload v2 containing her handshake message
|
||||
// and the (encrypted) transport message
|
||||
// The message is sent with a messageNametag equal to the one received through the QR code
|
||||
aliceStep, err := aliceHS.Step(nil, sentTransportMessage, messageNametag)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Bob reads Alice's payloads, and returns the (decrypted) transport message Alice sent to him
|
||||
// Note that Bob verifies if the received payloadv2 has the expected messageNametag set
|
||||
bobStep, err := bobHS.Step(aliceStep.PayloadV2, nil, messageNametag)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.True(t, bytes.Equal(bobStep.TransportMessage, sentTransportMessage))
|
||||
|
||||
// We generate an authorization code using the handshake state
|
||||
aliceAuthcode, err := aliceHS.Authcode()
|
||||
require.NoError(t, err)
|
||||
|
||||
bobAuthcode, err := bobHS.Authcode()
|
||||
require.NoError(t, err)
|
||||
|
||||
// We check that they are equal. Note that this check has to be confirmed with a user interaction.
|
||||
require.Equal(t, aliceAuthcode, bobAuthcode)
|
||||
|
||||
// 2nd step
|
||||
// <- sB, eAsB {r}
|
||||
|
||||
// Alice and Bob update their local next messageNametag using the available handshake information
|
||||
// During the handshake, messageNametag = HKDF(h), where h is the handshake hash value at the end of the last processed message
|
||||
aliceMessageNametag, err := aliceHS.ToMessageNametag()
|
||||
require.NoError(t, err)
|
||||
|
||||
bobMessageNametag, err := bobHS.ToMessageNametag()
|
||||
require.NoError(t, err)
|
||||
|
||||
// We set as a transport message the commitment randomness r
|
||||
sentTransportMessage = r
|
||||
|
||||
// At this step, Bob writes and returns a payload
|
||||
bobStep, err = bobHS.Step(nil, sentTransportMessage, bobMessageNametag)
|
||||
require.NoError(t, err)
|
||||
|
||||
// While Alice reads and returns the (decrypted) transport message
|
||||
aliceStep, err = aliceHS.Step(bobStep.PayloadV2, nil, aliceMessageNametag)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, aliceStep.TransportMessage, sentTransportMessage)
|
||||
|
||||
// Alice further checks if Bob's commitment opens to Bob's static key she just received
|
||||
expectedBobCommittedStaticKey := CommitPublicKey(WakuPairing.hashFn, aliceHS.hs.rs, aliceStep.TransportMessage)
|
||||
require.True(t, bytes.Equal(expectedBobCommittedStaticKey, bobCommittedStaticKey))
|
||||
|
||||
// 3rd step
|
||||
// -> sA, sAeB, sAsB {s}
|
||||
|
||||
// Alice and Bob update their local next messageNametag using the available handshake information
|
||||
aliceMessageNametag, err = aliceHS.ToMessageNametag()
|
||||
require.NoError(t, err)
|
||||
|
||||
bobMessageNametag, err = bobHS.ToMessageNametag()
|
||||
require.NoError(t, err)
|
||||
|
||||
// We set as a transport message the commitment randomness s
|
||||
sentTransportMessage = s
|
||||
|
||||
// Similarly as in first step, Alice writes a Waku2 payload containing the handshake message and the (encrypted) transport message
|
||||
aliceStep, err = aliceHS.Step(nil, sentTransportMessage, aliceMessageNametag)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Bob reads Alice's payloads, and returns the (decrypted) transport message Alice sent to him
|
||||
bobStep, err = bobHS.Step(aliceStep.PayloadV2, nil, bobMessageNametag)
|
||||
require.NoError(t, err)
|
||||
require.True(t, bytes.Equal(bobStep.TransportMessage, sentTransportMessage))
|
||||
|
||||
// Bob further checks if Alice's commitment opens to Alice's static key he just received
|
||||
expectedAliceCommittedStaticKey := CommitPublicKey(WakuPairing.hashFn, bobHS.hs.rs, bobStep.TransportMessage)
|
||||
|
||||
require.True(t, bytes.Equal(expectedAliceCommittedStaticKey, aliceCommittedStaticKey))
|
||||
|
||||
// Secure Transfer Phase
|
||||
// ==========
|
||||
|
||||
aliceHSResult, err := aliceHS.FinalizeHandshake()
|
||||
require.NoError(t, err)
|
||||
|
||||
bobHSResult, err := bobHS.FinalizeHandshake()
|
||||
require.NoError(t, err)
|
||||
|
||||
// We test read/write of random messages exchanged between Alice and Bob
|
||||
// Note that we exchange more than the number of messages contained in the nametag buffer to test if they are filled correctly as the communication proceeds
|
||||
for i := 0; i < 1; i++ { //10*MessageNametagBufferSize; i++ {
|
||||
// Alice writes to Bob
|
||||
message := generateRandomBytes(t, 32)
|
||||
payload, err := aliceHSResult.WriteMessage(message, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
readMessage, err := bobHSResult.ReadMessage(payload, nil)
|
||||
require.NoError(t, err)
|
||||
require.True(t, bytes.Equal(message, readMessage))
|
||||
|
||||
// Bob writes to Alice
|
||||
message = generateRandomBytes(t, 32)
|
||||
payload, err = bobHSResult.WriteMessage(message, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
readMessage, err = aliceHSResult.ReadMessage(payload, nil)
|
||||
require.NoError(t, err)
|
||||
require.True(t, bytes.Equal(message, readMessage))
|
||||
}
|
||||
|
||||
// We test how nametag buffers help in detecting lost messages
|
||||
// Alice writes two messages to Bob, but only the second is received
|
||||
message := generateRandomBytes(t, 32)
|
||||
_, err = aliceHSResult.WriteMessage(message, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
message = generateRandomBytes(t, 32)
|
||||
payload2, err := aliceHSResult.WriteMessage(message, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = bobHSResult.ReadMessage(payload2, nil)
|
||||
require.Error(t, err)
|
||||
require.ErrorIs(t, err, ErrNametagNotExpected)
|
||||
|
||||
// We adjust bob nametag buffer for next test (i.e. the missed message is correctly recovered)
|
||||
bobHS.hsResult.nametagsInbound.Delete(2)
|
||||
message = generateRandomBytes(t, 32)
|
||||
payload2, err = bobHSResult.WriteMessage(message, nil)
|
||||
require.NoError(t, err)
|
||||
readMessage, err := aliceHSResult.ReadMessage(payload2, nil)
|
||||
require.NoError(t, err)
|
||||
require.True(t, bytes.Equal(message, readMessage))
|
||||
|
||||
// We test if a missing nametag is correctly detected
|
||||
message = generateRandomBytes(t, 32)
|
||||
payload2, err = aliceHSResult.WriteMessage(message, nil)
|
||||
require.NoError(t, err)
|
||||
bobHS.hsResult.nametagsInbound.Delete(1)
|
||||
_, err = bobHSResult.ReadMessage(payload2, nil)
|
||||
require.ErrorIs(t, err, ErrNametagNotFound)
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user