Skip to content

Commit 3e9682d

Browse files
committed
Add CCM support
Branch with browserify#57 included
1 parent 85e0bd4 commit 3e9682d

File tree

10 files changed

+417
-87
lines changed

10 files changed

+417
-87
lines changed

ccm.js

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
var aes = require('./aes')
2+
var Buffer = require('safe-buffer').Buffer
3+
var Transform = require('cipher-base')
4+
var inherits = require('inherits')
5+
var xorInplace = require('buffer-xor/inplace')
6+
var xorTest = require('timing-safe-equal')
7+
function writeUIntBE (buff, value, start, length) {
8+
if (length > 6) {
9+
start += length - 6
10+
length = 6
11+
}
12+
buff.writeUIntBE(value, start, length)
13+
}
14+
15+
function cbc (prev, data, self) {
16+
var rump = 16 - (data.length % 16)
17+
if (rump !== 16) {
18+
data = Buffer.concat([data, Buffer.alloc(rump)])
19+
}
20+
var place = 0
21+
while (place < data.length) {
22+
xorInplace(prev, data.slice(place, place + 16))
23+
place += 16
24+
prev = self._cipher.encryptBlock(prev)
25+
}
26+
return prev
27+
}
28+
function StreamCipher (mode, key, iv, decrypt, options) {
29+
Transform.call(this)
30+
31+
if (!options || !options.authTagLength) throw new Error('options authTagLength is required')
32+
33+
if (options.authTagLength < 4 || options.authTagLength > 16 || options.authTagLength % 2 === 1) throw new Error('authTagLength must be one of 4, 6, 8, 10, 12, 14 or 16')
34+
35+
if (iv.length < 7 || iv.length > 13) throw new Error('iv must be between 7 and 13 bytes')
36+
37+
this._n = iv.length
38+
this._l = 15 - this._n
39+
this._cipher = new aes.AES(key)
40+
this.authTagLength = options.authTagLength
41+
this._mode = mode
42+
this._add = null
43+
this._decrypt = decrypt
44+
this._authTag = null
45+
this._called = false
46+
this._plainLength = null
47+
this._prev = null
48+
this._iv = iv
49+
this._cache = Buffer.allocUnsafe(0)
50+
this._failed = false
51+
this._firstBlock = null
52+
}
53+
function validSize (ivLen, chunkLen) {
54+
if (ivLen === 13 && chunkLen >= 65536) {
55+
return false
56+
}
57+
if (ivLen === 12 && chunkLen >= 16777216) {
58+
return false
59+
}
60+
return true
61+
}
62+
inherits(StreamCipher, Transform)
63+
function createTag (self, data) {
64+
var firstBlock = self._firstBlock
65+
if (!firstBlock) {
66+
firstBlock = Buffer.alloc(16)
67+
firstBlock[0] = ((self.authTagLength - 2) / 2) * 8 + self._l - 1
68+
self._iv.copy(firstBlock, 1)
69+
writeUIntBE(firstBlock, data.length, self._n + 1, self._l)
70+
firstBlock = self._cipher.encryptBlock(firstBlock)
71+
}
72+
return cbc(firstBlock, data, self)
73+
}
74+
StreamCipher.prototype._update = function (chunk) {
75+
if (this._called) throw new Error('Trying to add data in unsupported state')
76+
77+
if (!validSize(this._iv.length, chunk.length)) throw new Error('Message exceeds maximum size')
78+
79+
if (this._plainLength !== null && this._plainLength !== chunk.length) throw new Error('Trying to add data in unsupported state')
80+
81+
this._called = true
82+
this._prev = Buffer.alloc(16)
83+
this._prev[0] = this._l - 1
84+
this._iv.copy(this._prev, 1)
85+
var toXor
86+
if (this._decrypt) {
87+
toXor = this._mode.encrypt(this, Buffer.alloc(16)).slice(0, this.authTagLength)
88+
} else {
89+
this._authTag = this._mode.encrypt(this, createTag(this, chunk)).slice(0, this.authTagLength)
90+
}
91+
var out = this._mode.encrypt(this, chunk)
92+
if (this._decrypt) {
93+
var rawAuth = createTag(this, out).slice(0, this.authTagLength)
94+
xorInplace(rawAuth, toXor)
95+
this._failed = !xorTest(rawAuth, this._authTag)
96+
}
97+
this._cipher.scrub()
98+
return out
99+
}
100+
101+
StreamCipher.prototype._final = function () {
102+
if (this._decrypt && !this._authTag) throw new Error('Unsupported state or unable to authenticate data')
103+
104+
if (this._failed) throw new Error('Unsupported state or unable to authenticate data')
105+
}
106+
107+
StreamCipher.prototype.getAuthTag = function getAuthTag () {
108+
if (this._decrypt || !Buffer.isBuffer(this._authTag)) throw new Error('Attempting to get auth tag in unsupported state')
109+
110+
return this._authTag
111+
}
112+
113+
StreamCipher.prototype.setAuthTag = function setAuthTag (tag) {
114+
if (!this._decrypt) throw new Error('Attempting to set auth tag in unsupported state')
115+
116+
this._authTag = tag
117+
}
118+
119+
StreamCipher.prototype.setAAD = function setAAD (buf, options) {
120+
if (this._called) throw new Error('Attempting to set AAD in unsupported state')
121+
122+
if (!options || !options.plaintextLength) throw new Error('options plaintextLength is required')
123+
124+
if (!validSize(this._iv.length, options.plaintextLength)) throw new Error('Message exceeds maximum size')
125+
126+
this._plainLength = options.plaintextLength
127+
128+
if (!buf.length) return
129+
130+
var firstBlock = Buffer.alloc(16)
131+
firstBlock[0] = 64 + ((this.authTagLength - 2) / 2) * 8 + this._l - 1
132+
this._iv.copy(firstBlock, 1)
133+
writeUIntBE(firstBlock, options.plaintextLength, this._n + 1, this._l)
134+
firstBlock = this._cipher.encryptBlock(firstBlock)
135+
136+
var la = buf.length
137+
var ltag
138+
if (la < 65280) {
139+
ltag = Buffer.allocUnsafe(2)
140+
ltag.writeUInt16BE(la, 0)
141+
} else if (la < 4294967296) {
142+
ltag = Buffer.allocUnsafe(6)
143+
ltag[0] = 0xff
144+
ltag[1] = 0xfe
145+
ltag.writeUInt32BE(la, 2)
146+
} else {
147+
ltag = Buffer.alloc(10)
148+
ltag[0] = 0xff
149+
ltag[1] = 0xff
150+
ltag.writeUIntBE(la, 4, 6)
151+
}
152+
var aToAuth = Buffer.concat([ltag, buf])
153+
this._firstBlock = cbc(firstBlock, aToAuth, this)
154+
}
155+
156+
module.exports = StreamCipher

decrypter.js

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
var AuthCipher = require('./authCipher')
1+
var GCM = require('./gcm')
2+
var CCM = require('./ccm')
23
var Buffer = require('safe-buffer').Buffer
34
var MODES = require('./modes')
45
var StreamCipher = require('./streamCipher')
@@ -93,31 +94,33 @@ function unpad (last) {
9394
return last.slice(0, 16 - padded)
9495
}
9596

96-
function createDecipheriv (suite, password, iv) {
97+
function createDecipheriv (suite, password, iv, options) {
9798
var config = MODES[suite.toLowerCase()]
9899
if (!config) throw new TypeError('invalid suite type')
99100

100101
if (typeof iv === 'string') iv = Buffer.from(iv)
101-
if (config.mode !== 'GCM' && iv.length !== config.iv) throw new TypeError('invalid iv length ' + iv.length)
102+
if (config.type !== 'auth' && iv.length !== config.iv) throw new TypeError('invalid iv length ' + iv.length)
102103

103104
if (typeof password === 'string') password = Buffer.from(password)
104105
if (password.length !== config.key / 8) throw new TypeError('invalid key length ' + password.length)
105106

106107
if (config.type === 'stream') {
107108
return new StreamCipher(config.module, password, iv, true)
108-
} else if (config.type === 'auth') {
109-
return new AuthCipher(config.module, password, iv, true)
109+
} else if (config.mode === 'GCM') {
110+
return new GCM(config.module, password, iv, true)
111+
} else if (config.mode === 'CCM') {
112+
return new CCM(config.module, password, iv, true, options)
110113
}
111114

112115
return new Decipher(config.module, password, iv)
113116
}
114117

115-
function createDecipher (suite, password) {
118+
function createDecipher (suite, password, options) {
116119
var config = MODES[suite.toLowerCase()]
117120
if (!config) throw new TypeError('invalid suite type')
118121

119122
var keys = ebtk(password, false, config.key, config.iv)
120-
return createDecipheriv(suite, keys.key, keys.iv)
123+
return createDecipheriv(suite, keys.key, keys.iv, options)
121124
}
122125

123126
exports.createDecipher = createDecipher

encrypter.js

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
var MODES = require('./modes')
2-
var AuthCipher = require('./authCipher')
2+
var GCM = require('./gcm')
3+
var CCM = require('./ccm')
34
var Buffer = require('safe-buffer').Buffer
45
var StreamCipher = require('./streamCipher')
56
var Transform = require('cipher-base')
@@ -83,31 +84,33 @@ Splitter.prototype.flush = function () {
8384
return Buffer.concat([this.cache, padBuff])
8485
}
8586

86-
function createCipheriv (suite, password, iv) {
87+
function createCipheriv (suite, password, iv, options) {
8788
var config = MODES[suite.toLowerCase()]
8889
if (!config) throw new TypeError('invalid suite type')
8990

9091
if (typeof password === 'string') password = Buffer.from(password)
9192
if (password.length !== config.key / 8) throw new TypeError('invalid key length ' + password.length)
9293

9394
if (typeof iv === 'string') iv = Buffer.from(iv)
94-
if (config.mode !== 'GCM' && iv.length !== config.iv) throw new TypeError('invalid iv length ' + iv.length)
95+
if (config.type !== 'auth' && iv.length !== config.iv) throw new TypeError('invalid iv length ' + iv.length)
9596

9697
if (config.type === 'stream') {
9798
return new StreamCipher(config.module, password, iv)
98-
} else if (config.type === 'auth') {
99-
return new AuthCipher(config.module, password, iv)
99+
} else if (config.mode === 'GCM') {
100+
return new GCM(config.module, password, iv)
101+
} else if (config.mode === 'CCM') {
102+
return new CCM(config.module, password, iv, false, options)
100103
}
101104

102105
return new Cipher(config.module, password, iv)
103106
}
104107

105-
function createCipher (suite, password) {
108+
function createCipher (suite, password, options) {
106109
var config = MODES[suite.toLowerCase()]
107110
if (!config) throw new TypeError('invalid suite type')
108111

109112
var keys = ebtk(password, false, config.key, config.iv)
110-
return createCipheriv(suite, keys.key, keys.iv)
113+
return createCipheriv(suite, keys.key, keys.iv, options)
111114
}
112115

113116
exports.createCipheriv = createCipheriv

authCipher.js renamed to gcm.js

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,7 @@ var inherits = require('inherits')
55
var GHASH = require('./ghash')
66
var xor = require('buffer-xor')
77
var incr32 = require('./incr32')
8-
9-
function xorTest (a, b) {
10-
var out = 0
11-
if (a.length !== b.length) out++
12-
13-
var len = Math.min(a.length, b.length)
14-
for (var i = 0; i < len; ++i) {
15-
out += (a[i] ^ b[i])
16-
}
17-
18-
return out
19-
}
20-
8+
var xorTest = require('timing-safe-equal')
219
function calcIv (self, iv, ck) {
2210
if (iv.length === 12) {
2311
self._finID = Buffer.concat([iv, Buffer.from([0, 0, 0, 1])])
@@ -34,7 +22,7 @@ function calcIv (self, iv, ck) {
3422
ghash.update(Buffer.alloc(8, 0))
3523
var ivBits = len * 8
3624
var tail = Buffer.alloc(8)
37-
tail.writeUIntBE(ivBits, 0, 8)
25+
tail.writeUIntBE(ivBits, 2, 6)
3826
ghash.update(tail)
3927
self._finID = ghash.state
4028
var out = Buffer.from(self._finID)
@@ -89,7 +77,7 @@ StreamCipher.prototype._final = function () {
8977
if (this._decrypt && !this._authTag) throw new Error('Unsupported state or unable to authenticate data')
9078

9179
var tag = xor(this._ghash.final(this._alen * 8, this._len * 8), this._cipher.encryptBlock(this._finID))
92-
if (this._decrypt && xorTest(tag, this._authTag)) throw new Error('Unsupported state or unable to authenticate data')
80+
if (this._decrypt && !xorTest(tag, this._authTag)) throw new Error('Unsupported state or unable to authenticate data')
9381

9482
this._authTag = tag
9583
this._cipher.scrub()

modes/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ var modeModules = {
66
CFB1: require('./cfb1'),
77
OFB: require('./ofb'),
88
CTR: require('./ctr'),
9-
GCM: require('./ctr')
9+
GCM: require('./ctr'),
10+
CCM: require('./ctr')
1011
}
1112

1213
var modes = require('./list.json')

modes/list.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,5 +187,26 @@
187187
"iv": 12,
188188
"mode": "GCM",
189189
"type": "auth"
190+
},
191+
"aes-128-ccm": {
192+
"cipher": "AES",
193+
"key": 128,
194+
"iv": 12,
195+
"mode": "CCM",
196+
"type": "auth"
197+
},
198+
"aes-192-ccm": {
199+
"cipher": "AES",
200+
"key": 192,
201+
"iv": 12,
202+
"mode": "CCM",
203+
"type": "auth"
204+
},
205+
"aes-256-ccm": {
206+
"cipher": "AES",
207+
"key": 256,
208+
"iv": 12,
209+
"mode": "CCM",
210+
"type": "auth"
190211
}
191212
}

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@
3333
"create-hash": "^1.1.0",
3434
"evp_bytestokey": "^1.0.3",
3535
"inherits": "^2.0.1",
36-
"safe-buffer": "^5.0.1"
36+
"safe-buffer": "^5.0.1",
37+
"timing-safe-equal": "^1.0.0"
3738
},
3839
"devDependencies": {
3940
"standard": "^9.0.0",

scripts/populateFixtures.js

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
1-
var modes = require('./modes/list.json')
2-
var fixtures = require('./test/fixtures.json')
1+
var modes = require('../modes/list.json')
2+
var fixtures = require('../test/fixtures.json')
33
var crypto = require('crypto')
4-
var types = ['aes-128-cfb1', 'aes-192-cfb1', 'aes-256-cfb1']
5-
var ebtk = require('./EVP_BytesToKey')
4+
var types = ['aes-128-ccm', 'aes-192-ccm', 'aes-256-ccm']
5+
var ebtk = require('evp_bytestokey')
66
var fs = require('fs')
77

88
fixtures.forEach(function (fixture) {
99
types.forEach(function (cipher) {
10-
var suite = crypto.createCipher(cipher, new Buffer(fixture.password))
11-
var buf = new Buffer('')
12-
buf = Buffer.concat([buf, suite.update(new Buffer(fixture.text))])
13-
buf = Buffer.concat([buf, suite.final()])
14-
fixture.results.ciphers[cipher] = buf.toString('hex')
15-
if (modes[cipher].mode === 'ECB') {
16-
return
17-
}
18-
var suite2 = crypto.createCipheriv(cipher, ebtk(crypto, fixture.password, modes[cipher].key).key, new Buffer(fixture.iv, 'hex'))
19-
var buf2 = new Buffer('')
20-
buf2 = Buffer.concat([buf2, suite2.update(new Buffer(fixture.text))])
21-
buf2 = Buffer.concat([buf2, suite2.final()])
10+
var suite2 = crypto.createCipheriv(cipher, ebtk(fixture.password, false, modes[cipher].key).key, new Buffer(fixture.iv, 'hex').slice(0, 12), {
11+
authTagLength: 16
12+
})
13+
var text = Buffer.from(fixture.text)
14+
var aad = Buffer.from(fixture.aad, 'hex')
15+
console.log('aad', aad)
16+
suite2.setAAD(aad, {
17+
plaintextLength: text.length
18+
})
19+
var buf2 = suite2.update(text)
20+
suite2.final()
2221
fixture.results.cipherivs[cipher] = buf2.toString('hex')
22+
fixture.authtag[cipher] = suite2.getAuthTag().toString('hex')
2323
})
2424
})
2525
fs.writeFileSync('./test/fixturesNew.json', JSON.stringify(fixtures, false, 4))

0 commit comments

Comments
 (0)