Skip to content

Commit d1e300e

Browse files
authored
[box] Add library code to encrypt / decrypt messages (#42)
Turns out that we already have an asymmetric cryptography keypair in every Repl! But they're a ed25519 keypair, which is only used for signing messages. Well, it turns out that it is possible to convert an ed25519 keypair to a curve25519 keypair. And NaCl's public-key authenticated encryption primitive ([box](https://nacl.cr.yp.to/box.html)) uses curve25519. This change adds two library functions to encrypt a message address to a Repl given a Repl Identity token, and then have that Repl be able to decrypt it.
1 parent e1b8ab6 commit d1e300e

File tree

4 files changed

+130
-5
lines changed

4 files changed

+130
-5
lines changed

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Repl Identity
22

3-
Blog post on https://blog.replit.com coming soon!
3+
[![Go Reference](https://pkg.go.dev/badge/github.com/replit/go-replidentity.svg)](https://pkg.go.dev/github.com/replit/go-replidentity)
4+
5+
Blog post: https://blog.replit.com/repl-identity
46

57
Repl Identity stores a `REPL_IDENTITY` token in every Repl automatically. This
68
token is a signed [PASETO](https://paseto.io) that includes verifiable repl
@@ -11,4 +13,4 @@ This package provides the necessary code to verify these tokens.
1113
Check the example at `examples/extract.go` for an example usage. You can also
1214
see this in action at https://replit.com/@mattiselin/repl-identity. If you are
1315
logged in to Replit, you'll see your username when you click "Run" on the Cover
14-
Page - that's Repl Identity at work.
16+
Page - that's Repl Identity at work.

box.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package replidentity
2+
3+
import (
4+
"crypto/ed25519"
5+
"fmt"
6+
"io"
7+
8+
"golang.org/x/crypto/nacl/box"
9+
10+
"github.com/replit/go-replidentity/paserk"
11+
)
12+
13+
// SealAnonymousBox encrypts a message using the public key of the certificate.
14+
// Only the private key can decrypt the message.
15+
//
16+
// This uses
17+
// https://pkg.go.dev/golang.org/x/[email protected]/nacl/box#SealAnonymous, and
18+
// uses the ed25519 public key embedded in the certificate (converted to
19+
// curve25519 public key).
20+
func (v *VerifiedToken) SealAnonymousBox(message []byte, rand io.Reader) ([]byte, error) {
21+
pubkey, err := paserk.PASERKPublicToPublicKey(paserk.PASERKPublic(v.Certificate.GetPublicKey()))
22+
if err != nil {
23+
return nil, fmt.Errorf("paserk public key to ed25519 public key: %w", err)
24+
}
25+
26+
curve25519Pubkey, err := Ed25519PublicKeyToCurve25519(pubkey)
27+
if err != nil {
28+
return nil, fmt.Errorf("ed25519 public key to curve25519 public key: %w", err)
29+
}
30+
31+
result, err := box.SealAnonymous(
32+
nil,
33+
message,
34+
&curve25519Pubkey,
35+
rand,
36+
)
37+
if err != nil {
38+
return nil, fmt.Errorf("box.SealAnonymous: %w", err)
39+
}
40+
41+
return result, nil
42+
}
43+
44+
// OpenAnonymousBox decrypts a message encrypted with [SealAnonymousBox] using
45+
// the private key of the signature authority.
46+
//
47+
// This uses
48+
// https://pkg.go.dev/golang.org/x/[email protected]/nacl/box#OpenAnonymous, and
49+
// uses the ed25519 private key (converted to curve25519 private key).
50+
func (s *SigningAuthority) OpenAnonymousBox(sealedBox []byte) ([]byte, error) {
51+
curve25519Privkey := Ed25519PrivateKeyToCurve25519(s.privateKey)
52+
curve25519Pubkey, err := Ed25519PublicKeyToCurve25519(s.privateKey.Public().(ed25519.PublicKey))
53+
if err != nil {
54+
return nil, fmt.Errorf("ed25519 private key to curve25519 private key: %w", err)
55+
}
56+
57+
message, ok := box.OpenAnonymous(nil, sealedBox, &curve25519Pubkey, &curve25519Privkey)
58+
if !ok {
59+
return nil, fmt.Errorf("box.OpenAnonymous")
60+
}
61+
62+
return message, nil
63+
}

box_test.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package replidentity
2+
3+
import (
4+
"crypto/ed25519"
5+
"crypto/rand"
6+
"encoding/base64"
7+
"fmt"
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
13+
"github.com/replit/go-replidentity/paserk"
14+
"github.com/replit/go-replidentity/protos/external/goval/api"
15+
)
16+
17+
func TestBoxAnonymous(t *testing.T) {
18+
privkey, identity, err := identityToken("repl", "user", 1, "slug")
19+
require.NoError(t, err)
20+
21+
getPubKey := func(keyid, issuer string) (ed25519.PublicKey, error) {
22+
if keyid != developmentKeyID {
23+
return nil, nil
24+
}
25+
keyBytes, err := base64.StdEncoding.DecodeString(developmentPublicKey)
26+
if err != nil {
27+
return nil, fmt.Errorf("failed to parse public key as base64: %w", err)
28+
}
29+
30+
return ed25519.PublicKey(keyBytes), nil
31+
}
32+
33+
signingAuthority, err := NewSigningAuthority(
34+
string(paserk.PrivateKeyToPASERKSecret(privkey)),
35+
identity,
36+
"repl",
37+
getPubKey,
38+
)
39+
require.NoError(t, err)
40+
forwarded, err := signingAuthority.Sign("testing")
41+
require.NoError(t, err)
42+
43+
verifiedToken, err := VerifyToken(VerifyTokenOpts{
44+
Message: forwarded,
45+
Audience: []string{"testing"},
46+
GetPubKey: getPubKey,
47+
Flags: []api.FlagClaim{api.FlagClaim_IDENTITY},
48+
})
49+
require.NoError(t, err)
50+
51+
secret := "secret message"
52+
53+
sealedBox, err := verifiedToken.SealAnonymousBox([]byte(secret), rand.Reader)
54+
require.NoError(t, err)
55+
56+
unsealedBox, err := signingAuthority.OpenAnonymousBox(sealedBox)
57+
require.NoError(t, err)
58+
59+
assert.Equal(t, secret, string(unsealedBox))
60+
}

verify.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -381,9 +381,9 @@ type VerifiedToken struct {
381381
Certificate *api.GovalCert
382382
}
383383

384-
// VerifyToken verifies that the given `REPL_IDENTITY` value is in fact
385-
// signed by Goval's chain of authority, and addressed to the provided audience
386-
// (the `REPL_ID` of the recipient).
384+
// VerifyToken verifies that the given `REPL_IDENTITY` value is in fact signed
385+
// by Goval's chain of authority, and addressed to the provided audience (the
386+
// `REPL_ID` of the recipient). This is the preferred way of verifying tokens.
387387
//
388388
// The optional options allow specifying additional verifications on the identity.
389389
func VerifyToken(opts VerifyTokenOpts) (*VerifiedToken, error) {

0 commit comments

Comments
 (0)