| 1 | package crypto |
| 2 | |
| 3 | import ( |
| 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). |
| 11 | const PublicKeyLen = 65 |
| 12 | |
| 13 | // NonceLen is the 96-bit AES-GCM nonce every sealed object in CONTRA carries. |
| 14 | const 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. |
| 19 | var 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. |
| 26 | func 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. |
| 44 | func 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. |
| 57 | func 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. |
| 92 | func 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 | |