crypto icon indicating copy to clipboard operation
crypto copied to clipboard

Recommended process or abstraction for modern asymmetric encryption

Open cwebber opened this issue 6 years ago • 5 comments
trafficstars

Cryptographic right answers says about asymmetric encryption:

Of all the cryptographic “right answers”, this is the one you’re least likely to get right on your own. Don’t freelance public key encryption, and don’t use a low-level crypto library like OpenSSL or BouncyCastle.

Well, but I want to do want to use asymmetric encryption, and it's probably correct that unless (and maybe even if) I use RSA (which doesn't seem recommended anymore), I'm probably going to get this wrong.

What's recommended is that someone who moderately knows what they're doing implements all the pieces to give something that's safe-ish. What libsodium provides is its sealed boxes abstraction.

For my own selfish purposes, it would be extra nice if this could use the same encryption primitives as Tor v3 onion addresses, which is to say ed25519/curve25519. But this isn't strictly necessary.

cwebber avatar Nov 16 '19 19:11 cwebber

I guess the real question here is how to convert ed25519 to x25519 (aka curve25519). It looks like such a thing is possible.

EDIT: some more on this topic: https://crypto.stackexchange.com/questions/13077/can-curve25519-keys-be-used-with-ed25519-keys

cwebber avatar Nov 17 '19 14:11 cwebber

BTW, I figured out that I did not need this personally... I developed a handshake mechanism by which two users with signing keys agree on which encryption keys they would each like to use for their conversation. For my purposes that was good enough. It may still be that others would like a clear conversion mechanism, but maybe it's best to close this and if someone is still interested they can reopen this.

cwebber avatar Nov 18 '19 02:11 cwebber

Never mind, I'm reopening this. I think the process of doing something along the lines of a "crypto box" as described in libsodium is still useful considering that x25519 is used for key agreement and not something as "simple" interface-wise as a private key that you encrypt to.

cwebber avatar Nov 18 '19 17:11 cwebber

I tried writing an implementation that I think solves the issue. I'll be interested if you think it does also. It's inspired by how I believe libsodium's sealed boxes work:

#lang racket

(provide sealed-box)

(require crypto
         racket/random)

(struct sealed-box (type 1off-key encrypted)
  #:prefab)

;; From RFC7748:
;;
;; Both now share K = X25519(a, X25519(b, 9)) = X25519(b, X25519(a, 9))
;; as a shared secret.  Both MAY check, without leaking extra
;; information about the value of K, whether K is the all-zero value and
;; abort if so (see below).  Alice and Bob can then use a key-derivation
;; function that includes K, K_A, and K_B to derive a symmetric key.
;;
;; The check for the all-zero value results from the fact that the
;; X25519 function produces that value if it operates on an input
;; corresponding to a point with small order, where the order divides
;; the cofactor of the curve (see Section 7).  The check may be
;; performed by ORing all the bytes together and checking whether the
;; result is zero, as this eliminates standard side-channels in software
;; implementations.

(define (ensure-not-all-zeroes key)
  (when (zero?
         (for/fold ([result 0])
                   ([b key])
           (bitwise-ior result b)))
    (error 'all-zero-key)))

(define (smushed-iv bytes1 bytes2)
  (subbytes (sha256-bytes (bytes-append bytes1 bytes2))
            0 16))


(define/contract (seal-box to-key msg)
  (-> (and/c pk-key? pk-can-key-agree?) bytes? sealed-box?)
  (define ecx-impl
    (get-pk 'ecx (crypto-factories)))
  (define 1off-privkey
    (generate-private-key ecx-impl '((curve x25519))))
  (define 1off-pubkey-bytes
    (match (pk-key->datum 1off-privkey 'rkt-public)
      [(list 'ecx 'public 'x25519 pub)
       pub]))

  (define to-key-bytes
    (match (pk-key->datum to-key 'rkt-public)
      [(list 'ecx 'public 'x25519 pub)
       pub]))

  (define shared-symmetric-key
    (pk-derive-secret 1off-privkey to-key))

  ;; Protect against all-zero symmetric key
  (ensure-not-all-zeroes shared-symmetric-key)

  ;; TODO: maybe an all-zero iv is actually fine?
  ;;   I don't see any reason at all to generate an IV
  ;;   and yet it looks like libsodium is doing so
  (define iv
    (smushed-iv to-key-bytes 1off-pubkey-bytes))

  (define encrypted
    (encrypt '(aes ctr) shared-symmetric-key
             iv msg
             #:pad #t))

  (sealed-box 'x25519 1off-pubkey-bytes encrypted))

(define/contract (unseal-box to-key _sealed-box)
  (-> (and/c pk-key? pk-can-key-agree?) sealed-box? bytes?)
  (match-define (sealed-box 'x25519 1off-key-bytes encrypted)
    _sealed-box)

  (define to-key-bytes
    (match (pk-key->datum to-key 'rkt-public)
      [(list 'ecx 'public 'x25519 pub)
       pub]))

  (define 1off-key
    (datum->pk-key (list 'ecx 'public 'x25519 1off-key-bytes)
                   'rkt-public))

  (define shared-symmetric-key
    (pk-derive-secret to-key 1off-key))

  ;; Protect against all-zero symmetric key
  (ensure-not-all-zeroes shared-symmetric-key)

  ;; TODO: see iv comment in seal-box
  (define iv
    (smushed-iv to-key-bytes 1off-key-bytes))

  (decrypt '(aes ctr) shared-symmetric-key iv encrypted
           #:pad #t))

(module+ test
  (require rackunit
           "../utils/install-factory.rkt")
  (install-default-factories!)

  (define ecx-impl
    (get-pk 'ecx (crypto-factories)))
  (define ecx-privk1
    (generate-private-key ecx-impl '((curve x25519))))
  (define ecx-pubk1
    (pk-key->public-only-key ecx-privk1))

  (define sealed-box1
    (seal-box ecx-pubk1 #"hello my friend"))
  
  (test-equal?
   "Box sealing/unsealing works"
   (unseal-box ecx-privk1 sealed-box1)
   #"hello my friend"))

Dual licensed under Apache v2 and LGPL v3+ if you want to look towards including this.

cwebber avatar Nov 18 '19 18:11 cwebber

It looks like you aren't authenticating the message. You should use AES-GCM or another AEAD encryption algorithm or add a MAC to the encrypted message.

See crypto-doc/examples/encryption-x25519.rkt for a similar algorithm and some comments, but using different primitives. Another difference with that example is that it uses a random nonce, whereas libsodium's sealed boxes use deterministic nonces.

I agree that it would be nice for the crypto library to include higher-level primitives, but I don't want to include anything that hasn't been written down precisely and discussed. The libsodium algorithms would qualify, but I'd prefer to have more than one implementation, and they use primitives like HChacha20 that (I think) are not generally exposed by other libraries. There's ECIES, but that's a family of things, and I'd like to find a reference for some specific instances that are widely believed to be okay.

I don't have time to dive into this right now, but I might have time around the holidays.

rmculpepper avatar Nov 24 '19 13:11 rmculpepper