Skip to content

CCM mode #57

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 156 additions & 0 deletions ccm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
var aes = require('./aes')
var Buffer = require('safe-buffer').Buffer
var Transform = require('cipher-base')
var inherits = require('inherits')
var xorInplace = require('buffer-xor/inplace')
var xorTest = require('timing-safe-equal')
function writeUIntBE (buff, value, start, length) {
if (length > 6) {
start += length - 6
length = 6
}
buff.writeUIntBE(value, start, length)
}

function cbc (prev, data, self) {
var rump = 16 - (data.length % 16)
if (rump !== 16) {
data = Buffer.concat([data, Buffer.alloc(rump)])
}
var place = 0
while (place < data.length) {
xorInplace(prev, data.slice(place, place + 16))
place += 16
prev = self._cipher.encryptBlock(prev)
}
return prev
}
function StreamCipher (mode, key, iv, decrypt, options) {
Transform.call(this)

if (!options || !options.authTagLength) throw new Error('options authTagLength is required')

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')

if (iv.length < 7 || iv.length > 13) throw new Error('iv must be between 7 and 13 bytes')

this._n = iv.length
this._l = 15 - this._n
this._cipher = new aes.AES(key)
this.authTagLength = options.authTagLength
this._mode = mode
this._add = null
this._decrypt = decrypt
this._authTag = null
this._called = false
this._plainLength = null
this._prev = null
this._iv = iv
this._cache = Buffer.allocUnsafe(0)
this._failed = false
this._firstBlock = null
}
function validSize (ivLen, chunkLen) {
if (ivLen === 13 && chunkLen >= 65536) {
return false
}
if (ivLen === 12 && chunkLen >= 16777216) {
return false
}
return true
}
inherits(StreamCipher, Transform)
function createTag (self, data) {
var firstBlock = self._firstBlock
if (!firstBlock) {
firstBlock = Buffer.alloc(16)
firstBlock[0] = ((self.authTagLength - 2) / 2) * 8 + self._l - 1
self._iv.copy(firstBlock, 1)
writeUIntBE(firstBlock, data.length, self._n + 1, self._l)
firstBlock = self._cipher.encryptBlock(firstBlock)
}
return cbc(firstBlock, data, self)
}
StreamCipher.prototype._update = function (chunk) {
if (this._called) throw new Error('Trying to add data in unsupported state')

if (!validSize(this._iv.length, chunk.length)) throw new Error('Message exceeds maximum size')

if (this._plainLength !== null && this._plainLength !== chunk.length) throw new Error('Trying to add data in unsupported state')

this._called = true
this._prev = Buffer.alloc(16)
this._prev[0] = this._l - 1
this._iv.copy(this._prev, 1)
var toXor
if (this._decrypt) {
toXor = this._mode.encrypt(this, Buffer.alloc(16)).slice(0, this.authTagLength)
} else {
this._authTag = this._mode.encrypt(this, createTag(this, chunk)).slice(0, this.authTagLength)
}
var out = this._mode.encrypt(this, chunk)
if (this._decrypt) {
var rawAuth = createTag(this, out).slice(0, this.authTagLength)
xorInplace(rawAuth, toXor)
this._failed = !xorTest(rawAuth, this._authTag)
}
this._cipher.scrub()
return out
}

StreamCipher.prototype._final = function () {
if (this._decrypt && !this._authTag) throw new Error('Unsupported state or unable to authenticate data')

if (this._failed) throw new Error('Unsupported state or unable to authenticate data')
}

StreamCipher.prototype.getAuthTag = function getAuthTag () {
if (this._decrypt || !Buffer.isBuffer(this._authTag)) throw new Error('Attempting to get auth tag in unsupported state')

return this._authTag
}

StreamCipher.prototype.setAuthTag = function setAuthTag (tag) {
if (!this._decrypt) throw new Error('Attempting to set auth tag in unsupported state')

this._authTag = tag
}

StreamCipher.prototype.setAAD = function setAAD (buf, options) {
if (this._called) throw new Error('Attempting to set AAD in unsupported state')

if (!options || !options.plaintextLength) throw new Error('options plaintextLength is required')

if (!validSize(this._iv.length, options.plaintextLength)) throw new Error('Message exceeds maximum size')

this._plainLength = options.plaintextLength

if (!buf.length) return

var firstBlock = Buffer.alloc(16)
firstBlock[0] = 64 + ((this.authTagLength - 2) / 2) * 8 + this._l - 1
this._iv.copy(firstBlock, 1)
writeUIntBE(firstBlock, options.plaintextLength, this._n + 1, this._l)
firstBlock = this._cipher.encryptBlock(firstBlock)

var la = buf.length
var ltag
if (la < 65280) {
ltag = Buffer.allocUnsafe(2)
ltag.writeUInt16BE(la, 0)
} else if (la < 4294967296) {
ltag = Buffer.allocUnsafe(6)
ltag[0] = 0xff
ltag[1] = 0xfe
ltag.writeUInt32BE(la, 2)
} else {
ltag = Buffer.alloc(10)
ltag[0] = 0xff
ltag[1] = 0xff
ltag.writeUIntBE(la, 4, 6)
}
var aToAuth = Buffer.concat([ltag, buf])
this._firstBlock = cbc(firstBlock, aToAuth, this)
}

module.exports = StreamCipher
17 changes: 10 additions & 7 deletions decrypter.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
var AuthCipher = require('./authCipher')
var GCM = require('./gcm')
var CCM = require('./ccm')
var Buffer = require('safe-buffer').Buffer
var MODES = require('./modes')
var StreamCipher = require('./streamCipher')
Expand Down Expand Up @@ -93,31 +94,33 @@ function unpad (last) {
return last.slice(0, 16 - padded)
}

function createDecipheriv (suite, password, iv) {
function createDecipheriv (suite, password, iv, options) {
var config = MODES[suite.toLowerCase()]
if (!config) throw new TypeError('invalid suite type')

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

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

if (config.type === 'stream') {
return new StreamCipher(config.module, password, iv, true)
} else if (config.type === 'auth') {
return new AuthCipher(config.module, password, iv, true)
} else if (config.mode === 'GCM') {
return new GCM(config.module, password, iv, true)
} else if (config.mode === 'CCM') {
return new CCM(config.module, password, iv, true, options)
}

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

function createDecipher (suite, password) {
function createDecipher (suite, password, options) {
var config = MODES[suite.toLowerCase()]
if (!config) throw new TypeError('invalid suite type')

var keys = ebtk(password, false, config.key, config.iv)
return createDecipheriv(suite, keys.key, keys.iv)
return createDecipheriv(suite, keys.key, keys.iv, options)
}

exports.createDecipher = createDecipher
Expand Down
17 changes: 10 additions & 7 deletions encrypter.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
var MODES = require('./modes')
var AuthCipher = require('./authCipher')
var GCM = require('./gcm')
var CCM = require('./ccm')
var Buffer = require('safe-buffer').Buffer
var StreamCipher = require('./streamCipher')
var Transform = require('cipher-base')
Expand Down Expand Up @@ -83,31 +84,33 @@ Splitter.prototype.flush = function () {
return Buffer.concat([this.cache, padBuff])
}

function createCipheriv (suite, password, iv) {
function createCipheriv (suite, password, iv, options) {
var config = MODES[suite.toLowerCase()]
if (!config) throw new TypeError('invalid suite type')

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

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

if (config.type === 'stream') {
return new StreamCipher(config.module, password, iv)
} else if (config.type === 'auth') {
return new AuthCipher(config.module, password, iv)
} else if (config.mode === 'GCM') {
return new GCM(config.module, password, iv)
} else if (config.mode === 'CCM') {
return new CCM(config.module, password, iv, false, options)
}

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

function createCipher (suite, password) {
function createCipher (suite, password, options) {
var config = MODES[suite.toLowerCase()]
if (!config) throw new TypeError('invalid suite type')

var keys = ebtk(password, false, config.key, config.iv)
return createCipheriv(suite, keys.key, keys.iv)
return createCipheriv(suite, keys.key, keys.iv, options)
}

exports.createCipheriv = createCipheriv
Expand Down
18 changes: 3 additions & 15 deletions authCipher.js → gcm.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,7 @@ var inherits = require('inherits')
var GHASH = require('./ghash')
var xor = require('buffer-xor')
var incr32 = require('./incr32')

function xorTest (a, b) {
var out = 0
if (a.length !== b.length) out++

var len = Math.min(a.length, b.length)
for (var i = 0; i < len; ++i) {
out += (a[i] ^ b[i])
}

return out
}

var xorTest = require('timing-safe-equal')
function calcIv (self, iv, ck) {
if (iv.length === 12) {
self._finID = Buffer.concat([iv, Buffer.from([0, 0, 0, 1])])
Expand All @@ -34,7 +22,7 @@ function calcIv (self, iv, ck) {
ghash.update(Buffer.alloc(8, 0))
var ivBits = len * 8
var tail = Buffer.alloc(8)
tail.writeUIntBE(ivBits, 0, 8)
tail.writeUIntBE(ivBits, 2, 6)
ghash.update(tail)
self._finID = ghash.state
var out = Buffer.from(self._finID)
Expand Down Expand Up @@ -89,7 +77,7 @@ StreamCipher.prototype._final = function () {
if (this._decrypt && !this._authTag) throw new Error('Unsupported state or unable to authenticate data')

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

this._authTag = tag
this._cipher.scrub()
Expand Down
3 changes: 2 additions & 1 deletion modes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ var modeModules = {
CFB1: require('./cfb1'),
OFB: require('./ofb'),
CTR: require('./ctr'),
GCM: require('./ctr')
GCM: require('./ctr'),
CCM: require('./ctr')
}

var modes = require('./list.json')
Expand Down
21 changes: 21 additions & 0 deletions modes/list.json
Original file line number Diff line number Diff line change
Expand Up @@ -187,5 +187,26 @@
"iv": 12,
"mode": "GCM",
"type": "auth"
},
"aes-128-ccm": {
"cipher": "AES",
"key": 128,
"iv": 12,
"mode": "CCM",
"type": "auth"
},
"aes-192-ccm": {
"cipher": "AES",
"key": 192,
"iv": 12,
"mode": "CCM",
"type": "auth"
},
"aes-256-ccm": {
"cipher": "AES",
"key": 256,
"iv": 12,
"mode": "CCM",
"type": "auth"
}
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
"create-hash": "^1.1.0",
"evp_bytestokey": "^1.0.3",
"inherits": "^2.0.1",
"safe-buffer": "^5.0.1"
"safe-buffer": "^5.0.1",
"timing-safe-equal": "^1.0.0"
},
"devDependencies": {
"standard": "^9.0.0",
Expand Down
32 changes: 16 additions & 16 deletions scripts/populateFixtures.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
var modes = require('./modes/list.json')
var fixtures = require('./test/fixtures.json')
var modes = require('../modes/list.json')
var fixtures = require('../test/fixtures.json')
var crypto = require('crypto')
var types = ['aes-128-cfb1', 'aes-192-cfb1', 'aes-256-cfb1']
var ebtk = require('./EVP_BytesToKey')
var types = ['aes-128-ccm', 'aes-192-ccm', 'aes-256-ccm']
var ebtk = require('evp_bytestokey')
var fs = require('fs')

fixtures.forEach(function (fixture) {
types.forEach(function (cipher) {
var suite = crypto.createCipher(cipher, new Buffer(fixture.password))
var buf = new Buffer('')
buf = Buffer.concat([buf, suite.update(new Buffer(fixture.text))])
buf = Buffer.concat([buf, suite.final()])
fixture.results.ciphers[cipher] = buf.toString('hex')
if (modes[cipher].mode === 'ECB') {
return
}
var suite2 = crypto.createCipheriv(cipher, ebtk(crypto, fixture.password, modes[cipher].key).key, new Buffer(fixture.iv, 'hex'))
var buf2 = new Buffer('')
buf2 = Buffer.concat([buf2, suite2.update(new Buffer(fixture.text))])
buf2 = Buffer.concat([buf2, suite2.final()])
var suite2 = crypto.createCipheriv(cipher, ebtk(fixture.password, false, modes[cipher].key).key, new Buffer(fixture.iv, 'hex').slice(0, 12), {
authTagLength: 16
})
var text = Buffer.from(fixture.text)
var aad = Buffer.from(fixture.aad, 'hex')
console.log('aad', aad)
suite2.setAAD(aad, {
plaintextLength: text.length
})
var buf2 = suite2.update(text)
suite2.final()
fixture.results.cipherivs[cipher] = buf2.toString('hex')
fixture.authtag[cipher] = suite2.getAuthTag().toString('hex')
})
})
fs.writeFileSync('./test/fixturesNew.json', JSON.stringify(fixtures, false, 4))
Loading