aboutsummaryrefslogtreecommitdiff
path: root/internal/contentenc/offsets.go
blob: d3b9d7abd07364e0ede263fe563cc867206f0757 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
package contentenc

import (
	"log"

	"github.com/rfjakob/gocryptfs/v2/internal/tlog"
)

// Contentenc methods that translate offsets between ciphertext and plaintext

// PlainOffToBlockNo converts a plaintext offset to the ciphertext block number.
func (be *ContentEnc) PlainOffToBlockNo(plainOffset uint64) uint64 {
	return plainOffset / be.plainBS
}

// CipherOffToBlockNo converts the ciphertext offset to the plaintext block number.
func (be *ContentEnc) CipherOffToBlockNo(cipherOffset uint64) uint64 {
	if cipherOffset < HeaderLen {
		log.Panicf("BUG: offset %d is inside the file header", cipherOffset)
	}
	return (cipherOffset - HeaderLen) / be.cipherBS
}

// BlockNoToCipherOff gets the ciphertext offset of block "blockNo"
func (be *ContentEnc) BlockNoToCipherOff(blockNo uint64) uint64 {
	return HeaderLen + blockNo*be.cipherBS
}

// BlockNoToPlainOff gets the plaintext offset of block "blockNo"
func (be *ContentEnc) BlockNoToPlainOff(blockNo uint64) uint64 {
	return blockNo * be.plainBS
}

// CipherSizeToPlainSize calculates the plaintext size `plainSize` from a
// ciphertext size `cipherSize` (in bytes).
//
// Not all ciphertext sizes are legal due to the per-block overheads.
// For an illegal cipherSize, we return a best guess plainSize.
func (be *ContentEnc) CipherSizeToPlainSize(cipherSize uint64) uint64 {
	// Zero-sized files stay zero-sized
	if cipherSize == 0 {
		return 0
	}

	if cipherSize == HeaderLen {
		// This can happen between createHeader() and Write() and is harmless.
		tlog.Debug.Printf("cipherSize %d == header size: interrupted write?\n", cipherSize)
		return 0
	}

	if cipherSize < HeaderLen {
		tlog.Warn.Printf("cipherSize %d < header size %d: corrupt file\n", cipherSize, HeaderLen)
		return 0
	}

	// If the last block is incomplete, pad it to 1 byte of plaintext
	// (= 33 bytes of ciphertext).
	lastBlockSize := (cipherSize - HeaderLen) % be.cipherBS
	if lastBlockSize > 0 && lastBlockSize <= be.BlockOverhead() {
		tmp := cipherSize - lastBlockSize + be.BlockOverhead() + 1
		tlog.Warn.Printf("cipherSize %d: incomplete last block (%d bytes), padding to %d bytes", cipherSize, lastBlockSize, tmp)
		cipherSize = tmp
	}

	// Block number at last byte
	blockNo := be.CipherOffToBlockNo(cipherSize - 1)
	blockCount := blockNo + 1

	overhead := be.BlockOverhead()*blockCount + HeaderLen

	if overhead > cipherSize {
		tlog.Warn.Printf("cipherSize %d < overhead %d: corrupt file\n", cipherSize, overhead)
		return 0
	}

	return cipherSize - overhead
}

// PlainSizeToCipherSize calculates the ciphertext size from a plaintext size.
func (be *ContentEnc) PlainSizeToCipherSize(plainSize uint64) uint64 {
	// Zero-sized files stay zero-sized
	if plainSize == 0 {
		return 0
	}
	return be.PlainOffToCipherOff(plainSize-1) + 1
}

// PlainOffToCipherOff tells you the highest ciphertext offset that is
// *guaranteed* to be written/read when you write/read at `plainOff`.
func (be *ContentEnc) PlainOffToCipherOff(plainOff uint64) uint64 {
	startOfBlock := be.BlockNoToCipherOff(be.PlainOffToBlockNo(plainOff))
	return startOfBlock + plainOff%be.PlainBS() + be.BlockOverhead()
}

// ExplodePlainRange splits a plaintext byte range into (possibly partial) blocks
// Returns an empty slice if length == 0.
func (be *ContentEnc) ExplodePlainRange(offset uint64, length uint64) []IntraBlock {
	var blocks []IntraBlock
	var nextBlock IntraBlock
	nextBlock.fs = be

	for length > 0 {
		nextBlock.BlockNo = be.PlainOffToBlockNo(offset)
		nextBlock.Skip = offset - be.BlockNoToPlainOff(nextBlock.BlockNo)

		// Minimum of remaining plaintext data and remaining space in the block
		nextBlock.Length = MinUint64(length, be.plainBS-nextBlock.Skip)

		blocks = append(blocks, nextBlock)
		offset += nextBlock.Length
		length -= nextBlock.Length
	}
	return blocks
}

// ExplodeCipherRange splits a ciphertext byte range into (possibly partial)
// blocks This is used in reverse mode when reading files
func (be *ContentEnc) ExplodeCipherRange(offset uint64, length uint64) []IntraBlock {
	var blocks []IntraBlock
	var nextBlock IntraBlock
	nextBlock.fs = be

	for length > 0 {
		nextBlock.BlockNo = be.CipherOffToBlockNo(offset)
		nextBlock.Skip = offset - be.BlockNoToCipherOff(nextBlock.BlockNo)

		// This block can carry up to "maxLen" payload bytes
		maxLen := be.cipherBS - nextBlock.Skip
		nextBlock.Length = maxLen
		// But if the user requested less, we truncate the block to "length".
		if length < maxLen {
			nextBlock.Length = length
		}

		blocks = append(blocks, nextBlock)
		offset += nextBlock.Length
		length -= nextBlock.Length
	}
	return blocks
}

// BlockOverhead returns the per-block overhead.
func (be *ContentEnc) BlockOverhead() uint64 {
	return be.cipherBS - be.plainBS
}

// MinUint64 returns the minimum of two uint64 values.
func MinUint64(x uint64, y uint64) uint64 {
	if x < y {
		return x
	}
	return y
}