client: init e2ee envelopes

Kim committed Oct 22, 2025 at 19:38 UTC 401609a5a93da31471661b0297773d553dcbaa4d
2 files changed +106 -1
go.mod
+1 -1
@@ -11,6 +11,7 @@ require (
11 github.com/multiformats/go-multiaddr v0.16.0
12 github.com/rs/zerolog v1.34.0
13 github.com/spf13/cobra v1.10.1
14 + golang.org/x/crypto v0.41.0
15 )
16
17 require (
@@ -105,7 +106,6 @@ require (
106 go.uber.org/mock v0.5.2 // indirect
107 go.uber.org/multierr v1.11.0 // indirect
108 go.uber.org/zap v1.27.0 // indirect
108 - golang.org/x/crypto v0.41.0 // indirect
109 golang.org/x/exp v0.0.0-20250606033433-dcc06ee1d476 // indirect
110 golang.org/x/mod v0.27.0 // indirect
111 golang.org/x/net v0.43.0 // indirect
sdk/go/e2ee.go new
+105
@@ -0,0 +1,105 @@
1 +package sdk
2 +
3 +import (
4 + "crypto/aes"
5 + "crypto/cipher"
6 + crand "crypto/rand"
7 + "crypto/sha256"
8 + "encoding/base64"
9 + "errors"
10 + "os"
11 + "path/filepath"
12 + "strings"
13 +
14 + "golang.org/x/crypto/pbkdf2"
15 +)
16 +
17 +// Envelope is a simple password-derived AES-GCM container used by examples.
18 +// Fields are base64-encoded to keep the JSON compact and interoperable with browser JS.
19 +type Envelope struct {
20 + Salt string `json:"salt"`
21 + Nonce string `json:"nonce"`
22 + Ct string `json:"ct"`
23 +}
24 +
25 +// EnsurePSKAtPath loads a pre-shared key (PSK) from the given file path.
26 +// If missing/empty, generates a random base64url string, writes it, and returns it.
27 +func EnsurePSKAtPath(path string) (string, error) {
28 + if data, err := os.ReadFile(path); err == nil {
29 + if s := strings.TrimSpace(string(data)); s != "" {
30 + return s, nil
31 + }
32 + }
33 + if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
34 + return "", err
35 + }
36 + buf := make([]byte, 16)
37 + if _, err := crand.Read(buf); err != nil {
38 + return "", err
39 + }
40 + psk := base64.RawURLEncoding.EncodeToString(buf)
41 + if err := os.WriteFile(path, []byte(psk+"\n"), 0o600); err != nil {
42 + return "", err
43 + }
44 + return psk, nil
45 +}
46 +
47 +func deriveKey(psk string, salt []byte) []byte {
48 + return pbkdf2.Key([]byte(psk), salt, 100000, 32, sha256.New)
49 +}
50 +
51 +// DecryptEnvelope decrypts the given envelope using a key derived from psk+salt.
52 +func DecryptEnvelope(psk string, env Envelope) ([]byte, error) {
53 + salt, err := base64.StdEncoding.DecodeString(env.Salt)
54 + if err != nil {
55 + return nil, err
56 + }
57 + nonce, err := base64.StdEncoding.DecodeString(env.Nonce)
58 + if err != nil {
59 + return nil, err
60 + }
61 + ct, err := base64.StdEncoding.DecodeString(env.Ct)
62 + if err != nil {
63 + return nil, err
64 + }
65 + if len(nonce) != 12 {
66 + return nil, errors.New("bad nonce")
67 + }
68 + key := deriveKey(psk, salt)
69 + block, err := aes.NewCipher(key)
70 + if err != nil {
71 + return nil, err
72 + }
73 + aead, err := cipher.NewGCM(block)
74 + if err != nil {
75 + return nil, err
76 + }
77 + return aead.Open(nil, nonce, ct, nil)
78 +}
79 +
80 +// EncryptEnvelope encrypts pt with a key derived from psk and a fresh random salt/nonce.
81 +func EncryptEnvelope(psk string, pt []byte) (Envelope, error) {
82 + var env Envelope
83 + salt := make([]byte, 16)
84 + if _, err := crand.Read(salt); err != nil {
85 + return env, err
86 + }
87 + nonce := make([]byte, 12)
88 + if _, err := crand.Read(nonce); err != nil {
89 + return env, err
90 + }
91 + key := deriveKey(psk, salt)
92 + block, err := aes.NewCipher(key)
93 + if err != nil {
94 + return env, err
95 + }
96 + aead, err := cipher.NewGCM(block)
97 + if err != nil {
98 + return env, err
99 + }
100 + ct := aead.Seal(nil, nonce, pt, nil)
101 + env.Salt = base64.StdEncoding.EncodeToString(salt)
102 + env.Nonce = base64.StdEncoding.EncodeToString(nonce)
103 + env.Ct = base64.StdEncoding.EncodeToString(ct)
104 + return env, nil
105 +}