cryptol/examples/MiniLock/File.md
2018-10-10 16:21:38 -07:00

10 KiB

Copyright (c) 2013-2016 Galois, Inc. Distributed under the terms of the BSD3 license (see LICENSE file)

Define the minilock file format, encoding only.

module File where

import CfrgCurves
import Blake2s
import Base64
import Poly1305
import Salsa20
import CryptoBox
import Keys

The file format is:

miniLock magic bytes (8 bytes) Header length in bytes (4 bytes, little-endian) Header bytes (JSON, fields sometimes encrypted) Ciphertext bytes

Variable sized formats are potentially awkward for Cryptol because Cryptol encodes the length of values in the type. For example, the header size varies in the JS implementation. Fortunately this variablity is all due to either the arbitrary choices of the encoder or things deterministicly dependent on the input type (vs value). For an example of the later case, the number of recipients leads to a variable header size that is computable from the input type. The former is a larger issue - for example a Base58 encoding of 0 could be an arbitrary number of the letter 'A' ("A", "AA", etc). For encoding this is handled by emitting the maximum encoding (46 bytes for minilock identifications), but this issue prevents us from implementing a decoder that would interoperate with anything interesting.

The header format is:

{ version: Version of the miniLock protocol used for this file (Currently 1) (Number) ephemeral: Public key from ephemeral key pair used to encrypt decryptInfo object (Base64), decryptInfo: { (One copy of the below object for every recipient) Unique nonce for decrypting this object (Base64): { senderID: Sender's miniLock ID (Base58), recipientID: miniLock ID of this recipient (used for verfication) (Base58), fileInfo: { fileKey: Key for file decryption (Base64), fileNonce: Nonce for file decryption (Base64), fileHash: BLAKE2 hash (32 bytes) of the ciphertext bytes. (Base64) } (fileInfo is encrypted to recipient's public key using long-term key pair) (Base64), } (encrypted to recipient's public key using ephemeral key pair) (Base64) }

// Size of a given minilock container depends soley on the number of recipients and the plaintext
type MiniLockBytes nrRecip fileSize = 8 + 4 + 89 + (DecryptInfoSize + 1) * nrRecip + EncryptedBytes fileSize - 1

minilock : {nrRecip, fileNameBytes, msgBytes}
        ( fin nrRecip, fin fileNameBytes, fin msgBytes  // All the values are finite
        , nrRecip >= 1, 22 >= width nrRecip             // Between 1 and 4M recipients
        , 63 >= width msgBytes                          // Messages leq 2^63 bytes
        , fileNameBytes >= 1, 256 >= fileNameBytes      // Between 1 and 256 byte file name
        ) =>
        [nrRecip](MinilockID,[24][8])    // Recipients and nonces for the fileInfo field
        -> [fileNameBytes][8]            // File name
        -> [msgBytes][8]                 // Plaintext
        -> (Private25519, Public25519)   // Sender keys
        -> [32][8]                       // File key (random)
        -> [16][8]                       // File nonce (random)
        -> (Private25519, Public25519)   // Ephemeral keys (random)
        -> [MiniLockBytes nrRecip msgBytes][8]
minilock rs fileName0 msg senderKeys fileKey fileNonce ephemKeys = lockFile
 where
  filename               = take `{256} (fileName0 # (zero : [256][8]))
  ct                     = encryptData fileKey fileNonce filename msg
  fileInfo               = mkFileInfoBlob (fileKey, fileNonce, split (blake2s `{nn=32} ct))
  lockFile               = mkMiniLock rs senderKeys ephemKeys fileInfo ct

private
  magic : [8][8]
  magic = [0x6d, 0x69, 0x6e, 0x69, 0x4c, 0x6f, 0x63, 0x6b]

  encryptData : {msgBytes} (63 >= width msgBytes)
             => [32][8]-> [16][8] -> [256][8] -> [msgBytes][8] -> [EncryptedBytes msgBytes][8]
  encryptData k n f m = encryptChunks k n f cs cN
   where
    (cs,cN) = mkChunks m

  // encrypt chunks results in the ciphertext AND poly1305 tag for each chunk.
  encryptChunks : {chunks,rem}
                  (fin chunks, fin rem
                  , 32 >= width rem
                  , 64 >= width chunks)
               => [32][8] -> [16][8] -> [256][8] -> [chunks]Chunk -> [rem][8]
               -> [4 + 16 + 256 + chunks*((2^^20) + 4 + 16) + 4 + 16 + rem][8]
  encryptChunks key nonce c0 cs end = encChunk0 # join ctChunks # ctFinal
    where
    fullNonce0 = nonce # put64le zero
    encChunk0  = put32le 256      # crypto_secretbox c0 key fullNonce0 : [4 + 16 + 256][8]

    nonces     = [nonce # put64le i | i <- [1...]]
    ctChunks   = [put32le (2^^20) # crypto_secretbox cnk key n | n <- nonces | cnk <- cs]

    nFinal     =  nonce # put64le ((`chunks + 1) || 0x8000000000000000)
    ctFinal    =  put32le `rem     # crypto_secretbox end key nFinal

The minilock file format is a concatenation of the magic, header length field, header, and ciphertext.


  // Size of the 'decryptInfo' javascript record.
  type DecryptInfoSize = 549


  mkMiniLock : {nrRecip, ctBytes} (22 >= width nrRecip, nrRecip >= 1) =>
               [nrRecip](MinilockID,[24][8])
            -> (Private25519, Public25519)
            -> (Private25519, Public25519)
            -> FileInfoBlob
            -> [ctBytes][8]
            -> [8 + 4 + 89 + (DecryptInfoSize + 1) * nrRecip + ctBytes - 1][8]
  mkMiniLock rs senderKeys ephemKeys fileInfo ct =
      magic # put32le (length header) # header # ct
   where
    header = mkHeader rs senderKeys ephemKeys fileInfo

Construction of the "file info blob" means we build the plaintext value:

    {
        fileKey: Key for file decryption (Base64),
        fileNonce: Nonce for file decryption (Base64),
        fileHash: BLAKE2 hash (32 bytes) of the ciphertext bytes. (Base64)
    }
  // The triple of Key and nonce protecting the file along with the file's hash
  type FileInfo     = ([32][8], [16][8], [32][8])
  type FileInfoBlob = [155][8]

  mkFileInfoBlob : FileInfo
                -> FileInfoBlob
  mkFileInfoBlob (key,nonce,hash) =
      brace(
          jsonPair "fileKey"   (quote64 key)     # "," #
          jsonPair "fileNonce" (quote64 nonce) # "," #
          jsonPair "fileHash"  (quote64 hash)
      )

The header includes the public ephemeral key, version, and NrRecip copies of the fileinfo inside a decryptInfo field as such:

version: Version of the miniLock protocol used for this file (Currently 1) (Number) ephemeral: Public key from ephemeral key pair used to encrypt decryptInfo object (Base64), decryptInfo: { nonce1 : { encryptedFileInfo } , nonce2 : ... , ... }

  mkHeader : {nrRecip} (fin nrRecip, nrRecip >= 1)
            => [nrRecip](MinilockID,[24][8])
            -> (Private25519, Public25519)
            -> (Private25519, Public25519)
            -> FileInfoBlob
            -> [89 + (DecryptInfoSize+1) * nrRecip - 1][8]
  mkHeader rs senderKeys ephemKeys infoBlob =
      brace (
            jsonPair "version" "1" # ","
          # jsonPair "ephemeral" (quote64 ephemKeys.1) # ","
          # jsonPair "decryptInfo" (brace decInfoFull)
        )
   where
    ds = [ mkDecryptInfo senderKeys ephemKeys recvID recvNonce infoBlob  # "," | (recvID,recvNonce) <- rs ]
    // Drop the trailing comma
    decInfoFull = take `{back = min nrRecip 1} (join ds)

  // XXX return 'succ' for successful decode of the public id
  mkDecryptInfo :
                   (Private25519, Public25519)
                -> (Private25519, Public25519)
                -> MinilockID -> [24][8] -> FileInfoBlob -> [DecryptInfoSize][8]
  mkDecryptInfo senderKeys ephemKeys theirID nonce infoBlob = quote64 nonce # ":" # quote ct
   where
    ct        = encryptWith nonce (ephemKeys.0) theirPublic internals
    internals =   brace ( jsonPair "senderID"    (quote (encodeID senderKeys.1)) # ","
                        # jsonPair "recipientID" (quote theirID)                 # ","
                        # jsonPair "fileInfo"    (quote fileInfoCT)
                        )
    fileInfoCT  = encryptWith nonce (senderKeys.0) theirPublic infoBlob
    (succ,theirPublic) = decodeID theirID

  encryptWith : {n} (64 >= width (32 + n)) => [24][8] -> Private25519 -> Public25519 -> [n][8] -> [Enc64 (n+16)][8]
  encryptWith nonce secret public pt = base64enc (crypto_box pt nonce public secret)

  type NrChunks ptBytes = ptBytes / (2^^20)
  type LastChunkSize ptBytes = ptBytes - (NrChunks ptBytes) * 2^^20
  type EncryptedBytes ptBytes = 4 + 16 + 256 + (NrChunks ptBytes) * ((2^^20) + 4 + 16) + 4 + 16 + LastChunkSize ptBytes

  type ChunkSize = 2^^20
  type Chunk = [ChunkSize][8]
  type FullChunks bytes = bytes / ChunkSize
  type Rem bytes = bytes - FullChunks bytes * ChunkSize

  mkChunks :  {bytes} ( fin bytes )
           => [bytes][8] -> ([FullChunks bytes]Chunk, [Rem bytes][8])
  mkChunks pt = (cs,lst)
    where cs   = split (take `{front = FullChunks bytes * ChunkSize, back = Rem bytes} pt)
          lst = drop `{FullChunks bytes * ChunkSize} pt

The above code used some custom utility functions, which appear below.

  put32le : [32] -> [4][8]
  put32le x = reverse (split x)

  put64le : [64] -> [8][8]
  put64le x = reverse (split x)

  quote : {m} (fin m) => [m][8] -> [m+2][8]
  quote m = "\"" # m # "\""

  brace : {m} (fin m) => [m][8] -> [m+2][8]
  brace m = "{" # m # "}"

  quote64 : {m} (fin m) => [m][8] -> [4*(m + (3 - m % 3) % 3)/3 + 2][8]
  quote64 m = quote (base64enc m)

  jsonPair : {m,n} (fin m) => [m][8] -> [n][8] -> [3+m+n][8]
  jsonPair m n = quote m # ":" # n

// Encrypt a file such that one recipient
// User: "example@example.com
// Pass: "some bears eat all the honey in the jar"
//
// Can decrypt.
test_lock fname cont = file
  where
  myPriv    = testPriv
  myPub     = testPub
  theirID   = testID
  ephemPriv = zero # "ephemeral Curve25519"
  ephemPub  = Curve25519 ephemPriv basePoint25519
  nonceA    = zero # "recip1 nonce"
  nonceF    = zero # "file   nonce"
  key       = zero # "file key"
  file      = minilock [(theirID,nonceA)] fname cont (myPriv,myPub) key nonceF (ephemPriv,ephemPub)

test_construction : [MiniLockBytes 1 13][8]
test_construction = test_lock "some_filename" "some contents"