Skip to content

Commit b108fac

Browse files
committed
ReMemory Protocol V2: Shamir uses Raw Bytes instead of base64
1 parent 5805c4b commit b108fac

29 files changed

Lines changed: 609 additions & 88 deletions

internal/cmd/recover.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cmd
22

33
import (
44
"bytes"
5+
"encoding/base64"
56
"fmt"
67
"os"
78
"path/filepath"
@@ -101,15 +102,24 @@ func runRecover(cmd *cobra.Command, args []string) error {
101102
}
102103

103104
// Reconstruct passphrase
104-
passphrase, err := core.Combine(shareData)
105+
recovered, err := core.Combine(shareData)
105106
if err != nil {
106107
return fmt.Errorf("combining shares: %w", err)
107108
}
108109

110+
// v2 shares contain raw bytes; base64url-encode to get the age passphrase.
111+
// v1 shares contain the base64 string directly.
112+
var passphrase string
113+
if first.Version >= 2 {
114+
passphrase = base64.RawURLEncoding.EncodeToString(recovered)
115+
} else {
116+
passphrase = string(recovered)
117+
}
118+
109119
if recoverPassphrase {
110120
fmt.Println()
111121
fmt.Println("Recovered passphrase:")
112-
fmt.Println(string(passphrase))
122+
fmt.Println(passphrase)
113123
return nil
114124
}
115125

@@ -132,7 +142,7 @@ func runRecover(cmd *cobra.Command, args []string) error {
132142
}
133143

134144
var decryptedBuf bytes.Buffer
135-
if err := core.Decrypt(&decryptedBuf, bytes.NewReader(encryptedData), string(passphrase)); err != nil {
145+
if err := core.Decrypt(&decryptedBuf, bytes.NewReader(encryptedData), passphrase); err != nil {
136146
return fmt.Errorf("decryption failed (shares may be corrupted or from different operation): %w", err)
137147
}
138148

internal/cmd/seal.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"path/filepath"
88
"time"
99

10+
"encoding/base64"
11+
1012
"github.com/eljojo/rememory/internal/bundle"
1113
"github.com/eljojo/rememory/internal/core"
1214
"github.com/eljojo/rememory/internal/crypto"
@@ -104,8 +106,8 @@ func sealProject(p *project.Project, recoveryURL string) error {
104106
fmt.Printf(" Warning: %s\n", warning)
105107
}
106108

107-
// Generate passphrase
108-
passphrase, err := crypto.GeneratePassphrase(crypto.DefaultPassphraseBytes)
109+
// Generate passphrase (v2: split raw bytes, not the base64 string)
110+
raw, passphrase, err := crypto.GenerateRawPassphrase(crypto.DefaultPassphraseBytes)
109111
if err != nil {
110112
return fmt.Errorf("generating passphrase: %w", err)
111113
}
@@ -133,8 +135,8 @@ func sealProject(p *project.Project, recoveryURL string) error {
133135

134136
fmt.Printf("Splitting into %d shares (threshold: %d)...\n", len(p.Friends), p.Threshold)
135137

136-
// Split the passphrase
137-
shares, err := core.Split([]byte(passphrase), len(p.Friends), p.Threshold)
138+
// Split the raw bytes (v2: 32 bytes instead of 43-byte base64 string)
139+
shares, err := core.Split(raw, len(p.Friends), p.Threshold)
138140
if err != nil {
139141
return fmt.Errorf("splitting passphrase: %w", err)
140142
}
@@ -143,7 +145,7 @@ func sealProject(p *project.Project, recoveryURL string) error {
143145
shareInfos := make([]project.ShareInfo, len(shares))
144146
for i, shareData := range shares {
145147
friend := p.Friends[i]
146-
share := core.NewShare(i+1, len(p.Friends), p.Threshold, friend.Name, shareData)
148+
share := core.NewShare(2, i+1, len(p.Friends), p.Threshold, friend.Name, shareData)
147149

148150
filename := share.Filename()
149151
sharePath := filepath.Join(sharesDir, filename)
@@ -176,7 +178,7 @@ func sealProject(p *project.Project, recoveryURL string) error {
176178
fmt.Println("FAILED")
177179
return fmt.Errorf("verification failed: %w", err)
178180
}
179-
if string(recovered) != passphrase {
181+
if base64.RawURLEncoding.EncodeToString(recovered) != passphrase {
180182
fmt.Println("FAILED")
181183
return fmt.Errorf("verification failed: reconstructed passphrase doesn't match")
182184
}

internal/core/core_test.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ func TestValidateShamirParams(t *testing.T) {
192192
}
193193

194194
func TestShareEncodeDecode(t *testing.T) {
195-
original := NewShare(1, 5, 3, "Alice", []byte("test-share-data"))
195+
original := NewShare(1, 1, 5, 3, "Alice", []byte("test-share-data"))
196196

197197
encoded := original.Encode()
198198

@@ -225,7 +225,7 @@ func TestShareEncodeDecode(t *testing.T) {
225225
}
226226

227227
func TestShareVerify(t *testing.T) {
228-
share := NewShare(1, 5, 3, "Alice", []byte("test-data"))
228+
share := NewShare(1, 1, 5, 3, "Alice", []byte("test-data"))
229229

230230
// Valid checksum
231231
if err := share.Verify(); err != nil {
@@ -251,7 +251,7 @@ func TestShareFilename(t *testing.T) {
251251
}
252252

253253
for _, tt := range tests {
254-
share := NewShare(1, 3, 2, tt.holder, []byte("data"))
254+
share := NewShare(1, 1, 3, 2, tt.holder, []byte("data"))
255255
got := share.Filename()
256256
if got != tt.expected {
257257
t.Errorf("holder %q: got %q, want %q", tt.holder, got, tt.expected)
@@ -260,7 +260,7 @@ func TestShareFilename(t *testing.T) {
260260
}
261261

262262
func TestCompactEncodeRoundTrip(t *testing.T) {
263-
original := NewShare(1, 5, 3, "Alice", []byte("test-share-data-1234567890"))
263+
original := NewShare(1, 1, 5, 3, "Alice", []byte("test-share-data-1234567890"))
264264

265265
compact := original.CompactEncode()
266266

@@ -297,7 +297,7 @@ func TestCompactEncodeWithRealShares(t *testing.T) {
297297
}
298298

299299
for i, shareData := range shares {
300-
share := NewShare(i+1, 5, 3, "", shareData)
300+
share := NewShare(1, i+1, 5, 3, "", shareData)
301301
compact := share.CompactEncode()
302302

303303
decoded, err := ParseCompact(compact)
@@ -311,7 +311,7 @@ func TestCompactEncodeWithRealShares(t *testing.T) {
311311
}
312312

313313
func TestCompactEncodeFormat(t *testing.T) {
314-
share := NewShare(2, 5, 3, "Bob", []byte{0xDE, 0xAD, 0xBE, 0xEF})
314+
share := NewShare(1, 2, 5, 3, "Bob", []byte{0xDE, 0xAD, 0xBE, 0xEF})
315315
compact := share.CompactEncode()
316316

317317
if !strings.HasPrefix(compact, "RM1:") {
@@ -346,7 +346,7 @@ func TestCompactEncodeFormat(t *testing.T) {
346346

347347
func TestParseCompactRejectsBadInput(t *testing.T) {
348348
// Build a valid compact string to use as a base
349-
share := NewShare(1, 5, 3, "Alice", []byte("valid-data"))
349+
share := NewShare(1, 1, 5, 3, "Alice", []byte("valid-data"))
350350
valid := share.CompactEncode()
351351

352352
tests := []struct {
@@ -381,7 +381,7 @@ func TestParseCompactRejectsBadInput(t *testing.T) {
381381
func TestCompactEncodeNoHolderOrCreated(t *testing.T) {
382382
// Compact format intentionally omits Holder and Created metadata
383383
// to keep the string short for QR codes
384-
share := NewShare(3, 7, 4, "Carol with spaces", []byte("some-share-data"))
384+
share := NewShare(1, 3, 7, 4, "Carol with spaces", []byte("some-share-data"))
385385
compact := share.CompactEncode()
386386
decoded, err := ParseCompact(compact)
387387
if err != nil {

0 commit comments

Comments
 (0)