#!/usr/bin/env python
# -*- coding: utf-8 -*-
#=======================================================================
#
# keywrap.py
# ----------
# Python model to test AES KEY WRAP according to RFC 5649.
#
#
# Author: Joachim Strombergson
# Copyright (c) 2018, NORDUnet A/S
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
# - Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
#
# - Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# - Neither the name of the NORDUnet nor the names of its contributors may
# be used to endorse or promote products derived from this software
# without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#
#=======================================================================
#-------------------------------------------------------------------
# Python module imports.
#-------------------------------------------------------------------
import sys
import Crypto.Random
from Crypto.Cipher import AES
from struct import pack, unpack
#-------------------------------------------------------------------
# Constants.
#-------------------------------------------------------------------
VERBOSE = True
#-------------------------------------------------------------------
# AESKeyWrapWithPadding
#-------------------------------------------------------------------
class AESKeyWrapWithPadding(object):
"""
Implementation of AES Key Wrap With Padding from RFC 5649.
"""
class UnwrapError(Exception):
"Something went wrong during unwrap."
def __init__(self, key):
self.ctx = AES.new(key, AES.MODE_ECB)
def _encrypt(self, b1, b2):
aes_block = self.ctx.encrypt(b1 + b2)
return aes_block[:8], aes_block[8:]
def _decrypt(self, b1, b2):
aes_block = self.ctx.decrypt(b1 + b2)
return aes_block[:8], aes_block[8:]
@staticmethod
def _start_stop(start, stop): # Syntactic sugar
step = -1 if start > stop else 1
return xrange(start, stop + step, step)
@staticmethod
def _xor(R0, t):
return pack(">Q", unpack(">Q", R0)[0] ^ t)
def wrap(self, Q):
"RFC 5649 section 4.1."
m = len(Q) # Plaintext length
if m % 8 != 0: # Pad Q if needed
Q += "\x00" * (8 - (m % 8))
R = [pack(">LL", 0xa65959a6, m)] # Magic MSB(32,A), build LSB(32,A)
R.extend(Q[i : i + 8] # Append Q
for i in xrange(0, len(Q), 8))
n = len(R) - 1
if n == 1:
R[0], R[1] = self._encrypt(R[0], R[1])
else:
# RFC 3394 section 2.2.1
for j in self._start_stop(0, 5):
for i in self._start_stop(1, n):
R[0], R[i] = self._encrypt(R[0], R[i])
R[0] = self._xor(R[0], n * j + i)
assert len(R) == (n + 1) and all(len(r) == 8 for r in R)
return "".join(R)
def unwrap(self, C):
"RFC 5649 section 4.2."
if len(C) % 8 != 0:
raise self.UnwrapError("Ciphertext length {} is not an integral number of blocks"
.format(len(C)))
n = (len(C) / 8) - 1
R = [C[i : i + 8] for i in xrange(0, len(C), 8)]
if n == 1:
R[0], R[1] = self._decrypt(R[0], R[1])
else:
# RFC 3394 section 2.2.2 steps (1), (2), and part of (3)
for j in self._start_stop(5, 0):
for i in self._start_stop(n, 1):
R[0] = self._xor(R[0], n * j + i)
R[0], R[i] = self._decrypt(R[0], R[i])
magic, m = unpack(">LL", R[0])
if magic != 0xa65959a6:
raise self.UnwrapError("Magic value in AIV should have been 0xa65959a6, was 0x{:02x}"
.format(magic))
if m <= 8 * (n - 1) or m > 8 * n:
raise self.UnwrapError("Length encoded in AIV out of range: m {}, n {}".format(m, n))
R = "".join(R[1:])
assert len(R) == 8 * n
if any(r != "\x00" for r in R[m:]):
raise self.UnwrapError("Nonzero trailing bytes {}".format(R[m:].encode("hex")))
return R[:m]
#-------------------------------------------------------------------
# wrap_test1
#
# First, simplest test from NIST test vectors.
#-------------------------------------------------------------------
def wrap_test1():
my_key = Crypto.Random.new().read(256/8)
my_keywrap = AESKeyWrapWithPadding(my_key)
my_plaintext = "\x31\x32\x33"
my_wrap = my_keywrap.wrap(my_plaintext)
print(type(my_wrap))
my_unwrap = my_keywrap.wrap(my_wrap)
print(type(my_unwrap))
print("plaintext: %s wrapped: %s unwrapped: %s" %
(my_plaintext, my_wrap, my_unwrap))
#-------------------------------------------------------------------
#-------------------------------------------------------------------
def main():
print("Testing the Key Wrap Python model")
print("=================================")
print
# keywrap_aes_test()
wrap_test1()
#-------------------------------------------------------------------
# __name__
# Python thingy which allows the file to be run standalone as
# well as parsed from within a Python interpreter.
#-------------------------------------------------------------------
if __name__=="__main__":
# Run the main function.
sys.exit(main())
#=======================================================================
# EOF keywrap.py
#=======================================================================