Increase GCM IV size from 96 to 128 bits

This pushes back the birthday bound for collisions to make it virtually
irrelevant.
This commit is contained in:
Jakob Unterwurzacher 2015-12-19 14:41:39 +01:00
parent 88826dc51d
commit 1caa925868
21 changed files with 141 additions and 39 deletions

View File

@ -57,6 +57,11 @@ to mount the gocryptfs filesytem without user interaction.
**-fusedebug**
: Enable fuse library debug output
**-gcmiv128**
: Use an 128-bit IV for GCM encryption instead of Go's default of
96 bits (default true). This pushes back the birthday bound for IV
collisions far enough to make it irrelevant.
**-init**
: Initialize encrypted directory

View File

@ -18,7 +18,7 @@ File Contents
All file contents are encrypted using AES-256-GCM (Galois/Counter Mode).
Files are segmented into 4KB blocks. Each block gets a fresh random
96 bit IV each time it is modified. A 128-bit authentication tag (GHASH)
128 bit IV each time it is modified. A 128-bit authentication tag (GHASH)
protects each block from modifications.
Each file has a header containing a random 128-bit file ID. The

View File

@ -0,0 +1,32 @@
File Format
===========
Header
2 bytes header version (big endian uint16, currently 2)
16 bytes file id
Data block
16 bytes GCM IV (nonce)
1-4096 bytes encrypted data
16 bytes GHASH
Example: 1-byte file
--------------------
Header 18 bytes
Data block 33 bytes
Total: 51 bytes
Example: 5000-byte file
-----------------------
Header 18 bytes
Data block 4128 bytes
Data block 936 bytes
Total: 5082 bytes

View File

@ -44,7 +44,7 @@ func (be *CryptFS) CipherSizeToPlainSize(cipherSize uint64) uint64 {
blockNo := be.CipherOffToBlockNo(cipherSize - 1)
blockCount := blockNo + 1
overhead := BLOCK_OVERHEAD*blockCount + HEADER_LEN
overhead := be.BlockOverhead()*blockCount + HEADER_LEN
return cipherSize - overhead
}
@ -56,7 +56,7 @@ func (be *CryptFS) PlainSizeToCipherSize(plainSize uint64) uint64 {
blockNo := be.PlainOffToBlockNo(plainSize - 1)
blockCount := blockNo + 1
overhead := BLOCK_OVERHEAD*blockCount + HEADER_LEN
overhead := be.BlockOverhead()*blockCount + HEADER_LEN
return plainSize + overhead
}

View File

@ -46,6 +46,7 @@ func CreateConfFile(filename string, password string, plaintextNames bool, logN
cf.EncryptKey(key, password, logN)
// Set feature flags
cf.FeatureFlags = append(cf.FeatureFlags, FlagGCMIV128)
if plaintextNames {
cf.FeatureFlags = append(cf.FeatureFlags, FlagPlaintextNames)
} else {
@ -94,7 +95,7 @@ func LoadConfFile(filename string, password string) ([]byte, *ConfFile, error) {
// Unlock master key using password-based key
// We use stock go GCM instead of OpenSSL here as speed is not important
// and we get better error messages
cfs := NewCryptFS(scryptHash, false, false)
cfs := NewCryptFS(scryptHash, false, false, false)
key, err := cfs.DecryptBlock(cf.EncryptedKey, 0, nil)
if err != nil {
Warn.Printf("failed to unlock master key: %s\n", err.Error())
@ -115,7 +116,7 @@ func (cf *ConfFile) EncryptKey(key []byte, password string, logN int) {
scryptHash := cf.ScryptObject.DeriveKey(password)
// Lock master key using password-based key
cfs := NewCryptFS(scryptHash, false, false)
cfs := NewCryptFS(scryptHash, false, false, false)
cf.EncryptedKey = cfs.EncryptBlock(key, 0, nil)
}
@ -155,16 +156,18 @@ func (cf *ConfFile) WriteFile() error {
const (
// Understood Feature Flags.
// Also teach isFeatureFlagKnown() about any additions
// Also teach isFeatureFlagKnown() about any additions and
// add it to CreateConfFile() if you want to have it enabled by default.
FlagPlaintextNames = "PlaintextNames"
FlagDirIV = "DirIV"
FlagEMENames = "EMENames"
FlagGCMIV128 = "GCMIV128"
)
// Verify that we understand a feature flag
func (cf *ConfFile) isFeatureFlagKnown(flag string) bool {
switch flag {
case FlagPlaintextNames, FlagDirIV, FlagEMENames:
case FlagPlaintextNames, FlagDirIV, FlagEMENames, FlagGCMIV128:
return true
default:
return false

View File

@ -21,7 +21,7 @@ func TestSplitRange(t *testing.T) {
testRange{6654, 8945})
key := make([]byte, KEY_LEN)
f := NewCryptFS(key, true, false)
f := NewCryptFS(key, true, false, true)
for _, r := range ranges {
parts := f.ExplodePlainRange(r.offset, r.length)
@ -48,7 +48,7 @@ func TestCiphertextRange(t *testing.T) {
testRange{6654, 8945})
key := make([]byte, KEY_LEN)
f := NewCryptFS(key, true, false)
f := NewCryptFS(key, true, false, true)
for _, r := range ranges {
@ -70,7 +70,7 @@ func TestCiphertextRange(t *testing.T) {
func TestBlockNo(t *testing.T) {
key := make([]byte, KEY_LEN)
f := NewCryptFS(key, true, false)
f := NewCryptFS(key, true, false, true)
b := f.CipherOffToBlockNo(788)
if b != 0 {

View File

@ -11,9 +11,7 @@ import (
const (
DEFAULT_PLAINBS = 4096
KEY_LEN = 32 // AES-256
NONCE_LEN = 12
AUTH_TAG_LEN = 16
BLOCK_OVERHEAD = NONCE_LEN + AUTH_TAG_LEN
DIRIV_LEN = 16 // identical to AES block size
DIRIV_FILENAME = "gocryptfs.diriv"
)
@ -21,6 +19,8 @@ const (
type CryptFS struct {
blockCipher cipher.Block
gcm cipher.AEAD
gcmIVLen int
gcmIVGen nonceGenerator
plainBS uint64
cipherBS uint64
// Stores an all-zero block of size cipherBS
@ -29,7 +29,7 @@ type CryptFS struct {
DirIVCacheEnc DirIVCache
}
func NewCryptFS(key []byte, useOpenssl bool, plaintextNames bool) *CryptFS {
func NewCryptFS(key []byte, useOpenssl bool, plaintextNames bool, GCMIV128 bool) *CryptFS {
if len(key) != KEY_LEN {
panic(fmt.Sprintf("Unsupported key length %d", len(key)))
@ -40,22 +40,31 @@ func NewCryptFS(key []byte, useOpenssl bool, plaintextNames bool) *CryptFS {
panic(err)
}
// We want the IV size in bytes
gcmIV := 96 / 8
if GCMIV128 {
gcmIV = 128 / 8
}
var gcm cipher.AEAD
if useOpenssl {
gcm = opensslGCM{key}
} else {
gcm, err = cipher.NewGCM(b)
gcm, err = cipher.NewGCMWithNonceSize(b, gcmIV)
if err != nil {
panic(err)
}
}
cipherBS := DEFAULT_PLAINBS + NONCE_LEN + AUTH_TAG_LEN
plainBS := DEFAULT_PLAINBS
cipherBS := plainBS + gcmIV + AUTH_TAG_LEN
return &CryptFS{
blockCipher: b,
gcm: gcm,
plainBS: DEFAULT_PLAINBS,
gcmIVLen: gcmIV,
gcmIVGen: nonceGenerator{nonceLen: gcmIV},
plainBS: uint64(plainBS),
cipherBS: uint64(cipherBS),
allZeroBlock: make([]byte, cipherBS),
}
@ -65,3 +74,8 @@ func NewCryptFS(key []byte, useOpenssl bool, plaintextNames bool) *CryptFS {
func (be *CryptFS) PlainBS() uint64 {
return be.plainBS
}
// Per-block storage overhead
func (be *CryptFS) BlockOverhead() uint64 {
return be.cipherBS - be.plainBS
}

View File

@ -59,15 +59,15 @@ func (be *CryptFS) DecryptBlock(ciphertext []byte, blockNo uint64, fileId []byte
return make([]byte, be.plainBS), nil
}
if len(ciphertext) < NONCE_LEN {
if len(ciphertext) < be.gcmIVLen {
Warn.Printf("DecryptBlock: Block is too short: %d bytes\n", len(ciphertext))
return nil, errors.New("Block is too short")
}
// Extract nonce
nonce := ciphertext[:NONCE_LEN]
nonce := ciphertext[:be.gcmIVLen]
ciphertextOrig := ciphertext
ciphertext = ciphertext[NONCE_LEN:]
ciphertext = ciphertext[be.gcmIVLen:]
// Decrypt
var plaintext []byte
@ -94,7 +94,7 @@ func (be *CryptFS) EncryptBlock(plaintext []byte, blockNo uint64, fileID []byte)
}
// Get fresh nonce
nonce := gcmNonce.Get()
nonce := be.gcmIVGen.Get()
// Authenticate block with block number and file ID
aData := make([]byte, 8)

View File

@ -12,7 +12,7 @@ func TestEncryptPathNoIV(t *testing.T) {
s = append(s, "123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890")
key := make([]byte, KEY_LEN)
fs := NewCryptFS(key, true, false)
fs := NewCryptFS(key, true, false, true)
for _, n := range s {
c := fs.EncryptPathNoIV(n)
@ -33,7 +33,7 @@ func TestPad16(t *testing.T) {
s = append(s, []byte("12345678901234567abcdefg"))
key := make([]byte, KEY_LEN)
fs := NewCryptFS(key, true, false)
fs := NewCryptFS(key, true, false, true)
for i := range s {
orig := s[i]

View File

@ -24,16 +24,15 @@ func RandUint64() uint64 {
return binary.BigEndian.Uint64(b)
}
var gcmNonce nonce96
type nonce96 struct {
type nonceGenerator struct {
lastNonce []byte
nonceLen int // bytes
}
// Get a random 96 bit nonce
func (n *nonce96) Get() []byte {
nonce := RandBytes(12)
Debug.Printf("nonce96.Get(): %s\n", hex.EncodeToString(nonce))
func (n *nonceGenerator) Get() []byte {
nonce := RandBytes(n.nonceLen)
Debug.Printf("nonceGenerator.Get(): %s\n", hex.EncodeToString(nonce))
if bytes.Equal(nonce, n.lastNonce) {
m := fmt.Sprintf("Got the same nonce twice: %s. This should never happen!", hex.EncodeToString(nonce))
panic(m)

View File

@ -7,6 +7,7 @@ import (
"github.com/spacemonkeygo/openssl"
)
// Supports all nonce sizes
type opensslGCM struct {
key []byte
}
@ -16,13 +17,13 @@ func (be opensslGCM) Overhead() int {
}
func (be opensslGCM) NonceSize() int {
return NONCE_LEN
// We support any nonce size
return -1
}
// Seal encrypts and authenticates plaintext, authenticates the
// additional data and appends the result to dst, returning the updated
// slice. The nonce must be NonceSize() bytes long and unique for all
// time, for a given key.
// slice. opensslGCM supports any nonce size.
func (be opensslGCM) Seal(dst, nonce, plaintext, data []byte) []byte {
// Preallocate output buffer

View File

@ -0,0 +1 @@
cHxK1r_WYNd43oz_foCmzgt5jWrSvpiD-Ngy94L8LndrP9Kic-xlEg==

View File

@ -0,0 +1 @@
cdrpE7F_WZBEDSu1DI2k880I-9dsPjhD8AU8faPjh4omHmDcdcHlyimF

View File

@ -0,0 +1,16 @@
{
"EncryptedKey": "rjkwSNwi3nCUKMLaDttlYweHSDgyhbDx5sWv/a+h+cG1co5IXoXF9ZQSxXl1Qwm/XhY/dvTvnGZRREde",
"ScryptObject": {
"Salt": "mX6madEb9nbE+xgo840s9d2ro88f/5GuEiimQ+C7Z1I=",
"N": 65536,
"R": 8,
"P": 1,
"KeyLen": 32
},
"Version": 2,
"FeatureFlags": [
"GCMIV128",
"DirIV",
"EMENames"
]
}

View File

@ -0,0 +1 @@
¤7vßØFT5ÌÖË£<C38B>N×

View File

@ -60,7 +60,7 @@ func TestExampleFSv04(t *testing.T) {
checkExampleFS(t, pDir)
unmount(pDir)
mount(cDir, pDir, "-masterkey", "74676e34-0b47c145-00dac61a-17a92316-"+
"bb57044c-e205b71f-65f4fdca-7cabd4b3", "-diriv=false", "-emenames=false")
"bb57044c-e205b71f-65f4fdca-7cabd4b3", "-diriv=false", "-emenames=false", "-gcmiv128=false")
checkExampleFS(t, pDir)
unmount(pDir)
err = os.Remove(pDir)
@ -82,7 +82,7 @@ func TestExampleFSv05(t *testing.T) {
checkExampleFS(t, pDir)
unmount(pDir)
mount(cDir, pDir, "-masterkey", "199eae55-36bff4af-83b9a3a2-4fa16f65-"+
"1549ccdb-2d08d1f0-b1b26965-1b61f896", "-emenames=false")
"1549ccdb-2d08d1f0-b1b26965-1b61f896", "-emenames=false", "-gcmiv128=false")
checkExampleFS(t, pDir)
unmount(pDir)
err = os.Remove(pDir)
@ -104,7 +104,7 @@ func TestExampleFSv06(t *testing.T) {
checkExampleFS(t, pDir)
unmount(pDir)
mount(cDir, pDir, "-masterkey", "7bc8deb0-5fc894ef-a093da43-61561a81-"+
"0e8dee83-fdc056a4-937c37dd-9df5c520")
"0e8dee83-fdc056a4-937c37dd-9df5c520", "-gcmiv128=false")
checkExampleFS(t, pDir)
unmount(pDir)
err = os.Remove(pDir)
@ -113,8 +113,10 @@ func TestExampleFSv06(t *testing.T) {
}
}
// Test example_filesystems/v0.6
// Test example_filesystems/v0.6-plaintextnames
// with password mount and -masterkey mount
// v0.6 changed the file name handling a lot, hence the explicit test case for
// plaintextnames.
func TestExampleFSv06PlaintextNames(t *testing.T) {
pDir := tmpDir + "TestExampleFsV06PlaintextNames/"
cDir := "example_filesystems/v0.6-plaintextnames"
@ -126,7 +128,30 @@ func TestExampleFSv06PlaintextNames(t *testing.T) {
checkExampleFS(t, pDir)
unmount(pDir)
mount(cDir, pDir, "-masterkey", "f4690202-595e4593-64c4f7e0-4dddd7d1-"+
"303147f9-0ca8aea2-966341a7-52ea8ae9", "-plaintextnames")
"303147f9-0ca8aea2-966341a7-52ea8ae9", "-plaintextnames", "-gcmiv128=false")
checkExampleFS(t, pDir)
unmount(pDir)
err = os.Remove(pDir)
if err != nil {
t.Error(err)
}
}
// Test example_filesystems/v0.7
// with password mount and -masterkey mount
// v0.7 adds 128 bit GCM IVs
func TestExampleFSv07(t *testing.T) {
pDir := tmpDir + "TestExampleFsV07/"
cDir := "example_filesystems/v0.7"
err := os.Mkdir(pDir, 0777)
if err != nil {
t.Fatal(err)
}
mount(cDir, pDir, "-extpass", "echo test")
checkExampleFS(t, pDir)
unmount(pDir)
mount(cDir, pDir, "-masterkey", "bee8d0c5-74ec49ff-24b8793d-91d488a9-"+
"6117c58b-357eafaa-162ce3cf-8a061a28")
checkExampleFS(t, pDir)
unmount(pDir)
err = os.Remove(pDir)

View File

@ -35,7 +35,7 @@ const (
type argContainer struct {
debug, init, zerokey, fusedebug, openssl, passwd, foreground, version,
plaintextnames, quiet, diriv, emenames bool
plaintextnames, quiet, diriv, emenames, gcmiv128 bool
masterkey, mountpoint, cipherdir, cpuprofile, config, extpass string
notifypid, scryptn int
}
@ -149,6 +149,7 @@ func main() {
flagSet.BoolVar(&args.quiet, "q", false, "Quiet - silence informational messages")
flagSet.BoolVar(&args.diriv, "diriv", true, "Use per-directory file name IV")
flagSet.BoolVar(&args.emenames, "emenames", true, "Use EME filename encryption. This option implies diriv.")
flagSet.BoolVar(&args.gcmiv128, "gcmiv128", true, "Use an 128-bit IV for GCM encryption instead of Go's default of 96 bits")
flagSet.StringVar(&args.masterkey, "masterkey", "", "Mount with explicit master key")
flagSet.StringVar(&args.cpuprofile, "cpuprofile", "", "Write cpu profile to specified file")
flagSet.StringVar(&args.config, "config", "", "Use specified config file instead of CIPHERDIR/gocryptfs.conf")
@ -293,6 +294,7 @@ func pathfsFrontend(key []byte, args argContainer, confFile *cryptfs.ConfFile) *
PlaintextNames: args.plaintextnames,
DirIV: args.diriv,
EMENames: args.emenames,
GCMIV128: args.gcmiv128,
}
// confFile is nil when "-zerokey" or "-masterkey" was used
if confFile != nil {
@ -300,6 +302,7 @@ func pathfsFrontend(key []byte, args argContainer, confFile *cryptfs.ConfFile) *
frontendArgs.PlaintextNames = confFile.IsFeatureFlagSet(cryptfs.FlagPlaintextNames)
frontendArgs.DirIV = confFile.IsFeatureFlagSet(cryptfs.FlagDirIV)
frontendArgs.EMENames = confFile.IsFeatureFlagSet(cryptfs.FlagEMENames)
frontendArgs.GCMIV128 = confFile.IsFeatureFlagSet(cryptfs.FlagGCMIV128)
}
// EMENames implies DirIV, both on the command line and in the config file.
if frontendArgs.EMENames {

View File

@ -8,4 +8,5 @@ type Args struct {
PlaintextNames bool
DirIV bool
EMENames bool
GCMIV128 bool
}

View File

@ -266,7 +266,7 @@ func (f *file) doWrite(data []byte, off int64) (uint32, fuse.Status) {
blockOffset, blockLen := b.CiphertextRange()
blockData = f.cfs.EncryptBlock(blockData, b.BlockNo, f.header.Id)
cryptfs.Debug.Printf("ino%d: Writing %d bytes to block #%d, md5=%s\n",
f.ino, len(blockData)-cryptfs.BLOCK_OVERHEAD, b.BlockNo, cryptfs.Debug.Md5sum(blockData))
f.ino, uint64(len(blockData))-f.cfs.BlockOverhead(), b.BlockNo, cryptfs.Debug.Md5sum(blockData))
// Prevent partially written (=corrupt) blocks by preallocating the space beforehand
f.fdLock.Lock()

View File

@ -29,7 +29,7 @@ type FS struct {
// Encrypted FUSE overlay filesystem
func NewFS(args Args) *FS {
return &FS{
CryptFS: cryptfs.NewCryptFS(args.Masterkey, args.OpenSSL, args.PlaintextNames),
CryptFS: cryptfs.NewCryptFS(args.Masterkey, args.OpenSSL, args.PlaintextNames, args.GCMIV128),
FileSystem: pathfs.NewLoopbackFileSystem(args.Cipherdir),
args: args,
}