carlos / contra Public

Clone
git clone https://amadan.net/carlos/contra

Plain git — no account needed to clone.

Download

Download this file

1package crypto
2
3import (
4 "crypto/ecdh"
5 "crypto/rand"
6 "errors"
7 "fmt"
8)
9
10// PublicKeyLen is the SEC1 uncompressed P-256 point: 0x04 || X(32) || Y(32).
11const PublicKeyLen = 65
12
13// NonceLen is the 96-bit AES-GCM nonce every sealed object in CONTRA carries.
14const NonceLen = 12
15
16// ErrShortEnvelope means the bytes are too short to be an envelope at all,
17// which is a different fact from "the tag did not verify" and is worth its own
18// error — one is a truncated or wrong-shaped blob, the other is a wrong key.
19var ErrShortEnvelope = errors.New("crypto: handoff envelope is too short")
20
21// ECDH is the raw primitive: the X coordinate of d*P, big-endian at the field
22// width, with Y discarded per SEC 1 3.3.1.
23//
24// Pinned by vectors/contra-v1.json ("handoff"), whose generator checks OpenSSL
25// against RFC 5903 before it is allowed to produce a byte.
26func ECDH(scalar, peerPublic []byte) ([]byte, error) {
27 priv, err := ecdh.P256().NewPrivateKey(scalar)
28 if err != nil {
29 return nil, fmt.Errorf("crypto: handoff scalar: %w", err)
30 }
31 pub, err := ecdh.P256().NewPublicKey(peerPublic)
32 if err != nil {
33 return nil, fmt.Errorf("crypto: handoff peer key: %w", err)
34 }
35 return priv.ECDH(pub)
36}
37
38// ECIESKey derives the AEAD key for a message sealed to a static public key.
39//
40// Both public points go into the HKDF salt, not just the shared secret. The
41// shared secret alone names a relationship rather than a message: binding the
42// pair is what stops a recipient being talked into believing an envelope came
43// from an ephemeral key that did not produce it.
44func ECIESKey(ephemeralPublic, recipientPublic, shared []byte) ([]byte, error) {
45 salt := make([]byte, 0, 2*PublicKeyLen)
46 salt = append(salt, ephemeralPublic...)
47 salt = append(salt, recipientPublic...)
48 return Derive(shared, salt, LabelECIES, 32)
49}
50
51// SealTo seals a plaintext to a recipient's public key, minting a fresh
52// ephemeral keypair so the AEAD key is new even though the recipient's is not.
53//
54// The envelope is ephemeral_public(65) || nonce(12) || ciphertext || tag(16).
55// The AAD is the caller's, and for the device handoff it names the recipient's
56// own public key — so an envelope cannot be replayed at a second device.
57func SealTo(recipientPublic, aad, plaintext []byte) ([]byte, error) {
58 eph, err := ecdh.P256().GenerateKey(rand.Reader)
59 if err != nil {
60 return nil, err
61 }
62 ephPublic := eph.PublicKey().Bytes()
63
64 shared, err := ECDH(eph.Bytes(), recipientPublic)
65 if err != nil {
66 return nil, err
67 }
68 key, err := ECIESKey(ephPublic, recipientPublic, shared)
69 if err != nil {
70 return nil, err
71 }
72
73 nonce := make([]byte, NonceLen)
74 if _, err := rand.Read(nonce); err != nil {
75 return nil, err
76 }
77 sealed, err := Seal(key, nonce, aad, plaintext)
78 if err != nil {
79 return nil, err
80 }
81
82 out := make([]byte, 0, PublicKeyLen+NonceLen+len(sealed))
83 out = append(out, ephPublic...)
84 out = append(out, nonce...)
85 return append(out, sealed...), nil
86}
87
88// OpenFrom reverses SealTo using the recipient's own scalar. The recipient's
89// public key is passed in rather than recomputed because it is bound into the
90// derivation, and a caller that has cached the wrong one should be told so by
91// a failing tag rather than by silently deriving a different key.
92func OpenFrom(scalar, recipientPublic, aad, envelope []byte) ([]byte, error) {
93 if len(envelope) < PublicKeyLen+NonceLen {
94 return nil, ErrShortEnvelope
95 }
96 ephPublic := envelope[:PublicKeyLen]
97 nonce := envelope[PublicKeyLen : PublicKeyLen+NonceLen]
98
99 shared, err := ECDH(scalar, ephPublic)
100 if err != nil {
101 return nil, err
102 }
103 key, err := ECIESKey(ephPublic, recipientPublic, shared)
104 if err != nil {
105 return nil, err
106 }
107 return Open(key, nonce, aad, envelope[PublicKeyLen+NonceLen:])
108}
109