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
+}