Skip to content

Commit 6406314

Browse files
committed
mailbox: add mailbox package
1 parent 127eb31 commit 6406314

File tree

10 files changed

+2314
-33
lines changed

10 files changed

+2314
-33
lines changed

go.mod

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,19 @@
11
module github.com/lightninglabs/terminal-connect
22

33
require (
4-
github.com/golang/protobuf v1.4.3
5-
github.com/grpc-ecosystem/grpc-gateway v1.14.3
6-
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7 // indirect
7-
golang.org/x/sys v0.0.0-20210426080607-c94f62235c83 // indirect
8-
golang.org/x/text v0.3.3 // indirect
9-
google.golang.org/grpc v1.29.1
10-
google.golang.org/protobuf v1.23.0
4+
github.com/btcsuite/btcd v0.21.0-beta.0.20210513141527-ee5896bad5be
5+
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f
6+
github.com/golang/protobuf v1.5.2
7+
github.com/grpc-ecosystem/grpc-gateway v1.16.0
8+
github.com/kkdai/bstream v0.0.0-20181106074824-b3251f7901ec
9+
github.com/lightningnetwork/lnd v0.13.0-beta.rc5.0.20210727141148-c3962528e29f
10+
github.com/stretchr/testify v1.7.0
11+
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0
12+
google.golang.org/grpc v1.38.0
13+
google.golang.org/protobuf v1.26.0
14+
nhooyr.io/websocket v1.8.7
1115
)
1216

1317
replace git.schwanenlied.me/yawning/bsaes.git => github.com/Yawning/bsaes v0.0.0-20180720073208-c0276d75487e
1418

15-
// Fix incompatibility of etcd go.mod package.
16-
// See https://github.com/etcd-io/etcd/issues/11154
17-
replace go.etcd.io/etcd => go.etcd.io/etcd v0.5.0-alpha.5.0.20201125193152-8a03d2e9614b
18-
19-
replace (
20-
github.com/lightningnetwork/lnd => github.com/guggero/lnd v0.11.0-beta.rc4.0.20210601120905-1ba56b534c9f
21-
github.com/lightningnetwork/lnd/cert => github.com/guggero/lnd/cert v1.0.4-0.20210601120905-1ba56b534c9f
22-
github.com/lightningnetwork/lnd/healthcheck => github.com/guggero/lnd/healthcheck v0.0.0-20210601120905-1ba56b534c9f
23-
github.com/lightningnetwork/lnd/kvdb => github.com/guggero/lnd/kvdb v0.0.0-20210601120905-1ba56b534c9f
24-
)
25-
2619
go 1.16

go.sum

Lines changed: 780 additions & 15 deletions
Large diffs are not rendered by default.

mailbox/client_conn.go

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
package mailbox
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net"
7+
"regexp"
8+
9+
"github.com/grpc-ecosystem/grpc-gateway/runtime"
10+
"github.com/lightninglabs/terminal-connect/hashmailrpc"
11+
"nhooyr.io/websocket"
12+
)
13+
14+
var (
15+
receivePath = "/v1/terminal-connect/hashmail/receive"
16+
sendPath = "/v1/terminal-connect/hashmail/send"
17+
addrFormat = "wss://%s%s?method=POST"
18+
19+
resultPattern = regexp.MustCompile("{\"result\":(.*)}")
20+
defaultMarshaler = &runtime.JSONPb{OrigName: true, EmitDefaults: false}
21+
)
22+
23+
// ClientConn is a type that establishes a base transport connection to a
24+
// mailbox server using a REST/WebSocket connection. This type can be used to
25+
// initiate a mailbox transport connection from a browser/WASM environment.
26+
type ClientConn struct {
27+
*connKit
28+
29+
receiveSocket *websocket.Conn
30+
sendSocket *websocket.Conn
31+
32+
receiveInitialized bool
33+
}
34+
35+
// NewClientConn creates a new client connection with the given receive and send
36+
// session identifiers. The context given as the first parameter will be used
37+
// throughout the connection lifetime.
38+
func NewClientConn(ctx context.Context, receiveSID,
39+
sendSID [64]byte) *ClientConn {
40+
41+
c := &ClientConn{}
42+
c.connKit = &connKit{
43+
ctx: ctx,
44+
impl: c,
45+
receiveSID: receiveSID,
46+
sendSID: sendSID,
47+
}
48+
49+
return c
50+
}
51+
52+
// Dial returns a net.Conn abstraction over the mailbox connection.
53+
func (c *ClientConn) Dial(_ context.Context, serverHost string) (net.Conn,
54+
error) {
55+
56+
c.connKit.serverAddr = serverHost
57+
58+
connectMsg := NewMsgConnect(ProtocolVersion)
59+
return c, c.SendControlMsg(connectMsg)
60+
}
61+
62+
// ReceiveControlMsg tries to receive a control message over the underlying
63+
// mailbox connection.
64+
//
65+
// NOTE: This is part of the Conn interface.
66+
func (c *ClientConn) ReceiveControlMsg(receive ControlMsg) error {
67+
if c.receiveSocket == nil {
68+
receiveAddr := fmt.Sprintf(
69+
addrFormat, c.serverAddr, receivePath,
70+
)
71+
receiveSocket, _, err := websocket.Dial(c.ctx, receiveAddr, nil)
72+
if err != nil {
73+
return err
74+
}
75+
c.receiveSocket = receiveSocket
76+
}
77+
78+
if !c.receiveInitialized {
79+
receiveInit := &hashmailrpc.CipherBoxDesc{
80+
StreamId: c.receiveSID[:],
81+
}
82+
receiveInitBytes, err := defaultMarshaler.Marshal(receiveInit)
83+
if err != nil {
84+
return err
85+
}
86+
87+
err = c.receiveSocket.Write(
88+
c.ctx, websocket.MessageText, receiveInitBytes,
89+
)
90+
if err != nil {
91+
return err
92+
}
93+
94+
c.receiveInitialized = true
95+
}
96+
97+
_, msg, err := c.receiveSocket.Read(c.ctx)
98+
if err != nil {
99+
return err
100+
}
101+
unwrapped := stripJSONWrapper(string(msg))
102+
103+
mailboxMsg := &hashmailrpc.CipherBox{}
104+
err = defaultMarshaler.Unmarshal([]byte(unwrapped), mailboxMsg)
105+
if err != nil {
106+
return err
107+
}
108+
109+
return receive.Deserialize(mailboxMsg.Msg)
110+
}
111+
112+
// SendControlMsg tries to send a control message over the underlying mailbox
113+
// connection.
114+
//
115+
// NOTE: This is part of the Conn interface.
116+
func (c *ClientConn) SendControlMsg(controlMsg ControlMsg) error {
117+
if c.sendSocket == nil {
118+
sendAddr := fmt.Sprintf(addrFormat, c.serverAddr, sendPath)
119+
sendSocket, _, err := websocket.Dial(c.ctx, sendAddr, nil)
120+
if err != nil {
121+
return err
122+
}
123+
124+
c.sendSocket = sendSocket
125+
}
126+
127+
payloadBytes, err := controlMsg.Serialize()
128+
if err != nil {
129+
return err
130+
}
131+
132+
sendInit := &hashmailrpc.CipherBox{
133+
Desc: &hashmailrpc.CipherBoxDesc{
134+
StreamId: c.sendSID[:],
135+
},
136+
Msg: payloadBytes,
137+
}
138+
sendInitBytes, err := defaultMarshaler.Marshal(sendInit)
139+
if err != nil {
140+
return err
141+
}
142+
return c.sendSocket.Write(c.ctx, websocket.MessageText, sendInitBytes)
143+
}
144+
145+
// Close closes the underlying mailbox connection.
146+
//
147+
// NOTE: This is part of the net.Conn interface.
148+
func (c *ClientConn) Close() error {
149+
var returnErr error
150+
if c.receiveSocket != nil {
151+
returnErr = c.receiveSocket.Close(
152+
websocket.StatusGoingAway, "bye",
153+
)
154+
}
155+
if c.sendSocket != nil {
156+
returnErr = c.sendSocket.Close(
157+
websocket.StatusGoingAway, "bye",
158+
)
159+
}
160+
161+
return returnErr
162+
}
163+
164+
var _ Conn = (*ClientConn)(nil)
165+
166+
func stripJSONWrapper(wrapped string) string {
167+
return resultPattern.ReplaceAllString(wrapped, "${1}")
168+
}

mailbox/crypto.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package mailbox
2+
3+
import (
4+
"crypto/rand"
5+
"crypto/sha256"
6+
"math/big"
7+
8+
"github.com/btcsuite/btcd/btcec"
9+
"github.com/kkdai/bstream"
10+
"github.com/lightningnetwork/lnd/aezeed"
11+
"golang.org/x/crypto/chacha20poly1305"
12+
)
13+
14+
const (
15+
ClientPointPreimage = "TerminalConnectClient"
16+
ServerPointPreimage = "TerminalConnectServer"
17+
NumPasswordWords = 8 // TODO: make shorter by masking leftover bits.
18+
NumPasswordBytes = (NumPasswordWords * aezeed.BitsPerWord) / 8
19+
)
20+
21+
var (
22+
nonce = [12]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}
23+
)
24+
25+
// NewPassword generates a new one-time-use password, represented as a set of
26+
// mnemonic words and the raw entropy itself.
27+
func NewPassword() ([NumPasswordWords]string, [NumPasswordBytes]byte, error) {
28+
var (
29+
passwordEntropy [NumPasswordBytes]byte
30+
password [NumPasswordWords]string
31+
)
32+
if _, err := rand.Read(passwordEntropy[:]); err != nil {
33+
return password, passwordEntropy, err
34+
}
35+
36+
cipherBits := bstream.NewBStreamReader(passwordEntropy[:])
37+
for i := 0; i < NumPasswordWords; i++ {
38+
index, err := cipherBits.ReadBits(aezeed.BitsPerWord)
39+
if err != nil {
40+
return password, passwordEntropy, err
41+
}
42+
43+
password[i] = aezeed.DefaultWordList[index]
44+
}
45+
46+
return password, passwordEntropy, nil
47+
}
48+
49+
// PasswordMnemonicToEntropy reverses the mnemonic word encoding and returns the
50+
// raw password entropy bytes.
51+
func PasswordMnemonicToEntropy(
52+
password [NumPasswordWords]string) [NumPasswordBytes]byte {
53+
54+
var passwordEntropy [NumPasswordBytes]byte
55+
cipherBits := bstream.NewBStreamWriter(NumPasswordBytes)
56+
for _, word := range password {
57+
index := uint64(aezeed.ReverseWordMap[word])
58+
cipherBits.WriteBits(index, aezeed.BitsPerWord)
59+
}
60+
61+
copy(passwordEntropy[:], cipherBits.Bytes())
62+
63+
return passwordEntropy
64+
}
65+
66+
// TODO(guggero): Implement using "hash-and-increment" algorithm.
67+
func GeneratorPoint(preimage string) (*btcec.PublicKey, error) {
68+
hash := sha256.Sum256([]byte(preimage))
69+
_, pubKey := btcec.PrivKeyFromBytes(btcec.S256(), hash[:])
70+
return pubKey, nil
71+
}
72+
73+
// TODO(guggero): Implement in an actually secure way.
74+
func SPAKE2MaskPoint(ephemeralPubKey *btcec.PublicKey,
75+
generatorPointPreimage string, password []byte) (*btcec.PublicKey,
76+
error) {
77+
78+
g, err := GeneratorPoint(generatorPointPreimage)
79+
if err != nil {
80+
return nil, err
81+
}
82+
83+
// Use PBKDF2 here?
84+
pwHash := sha256.Sum256(password)
85+
blindingPoint := &btcec.PublicKey{}
86+
blindingPoint.X, blindingPoint.Y = btcec.S256().ScalarMult(
87+
g.X, g.Y, pwHash[:],
88+
)
89+
90+
result := &btcec.PublicKey{Curve: btcec.S256()}
91+
result.X, result.Y = btcec.S256().Add(
92+
blindingPoint.X, blindingPoint.Y,
93+
ephemeralPubKey.X, ephemeralPubKey.Y,
94+
)
95+
return result, nil
96+
}
97+
98+
// TODO(guggero): Implement in an actually secure way.
99+
func SPAKE2UnmaskPoint(blindedKey *btcec.PublicKey,
100+
generatorPointPreimage string, password []byte) (*btcec.PublicKey,
101+
error) {
102+
103+
g, err := GeneratorPoint(generatorPointPreimage)
104+
if err != nil {
105+
return nil, err
106+
}
107+
108+
// Use PBKDF2 here?
109+
pwHash := sha256.Sum256(password)
110+
blindingPoint := &btcec.PublicKey{}
111+
blindingPoint.X, blindingPoint.Y = btcec.S256().ScalarMult(
112+
g.X, g.Y, pwHash[:],
113+
)
114+
115+
result := &btcec.PublicKey{Curve: btcec.S256()}
116+
negY := new(big.Int).Neg(blindedKey.Y)
117+
negY = negY.Mod(negY, btcec.S256().P)
118+
result.X, result.Y = blindedKey.Curve.Add(
119+
blindedKey.X, blindedKey.Y, blindedKey.X, negY,
120+
)
121+
return result, nil
122+
}
123+
124+
// TODO(guggero): Implement in an actually secure way.
125+
func Encrypt(plainText []byte, secret []byte) ([]byte, error) {
126+
cipher, _ := chacha20poly1305.New(secret)
127+
128+
return cipher.Seal(nil, nonce[:], plainText, nil), nil
129+
}
130+
131+
// TODO(guggero): Implement in an actually secure way.
132+
func Decrypt(cipherText []byte, secret []byte) ([]byte, error) {
133+
cipher, _ := chacha20poly1305.New(secret)
134+
135+
return cipher.Open(nil, nonce[:], cipherText, nil)
136+
}

0 commit comments

Comments
 (0)