Repository: OpenMined/PySyft
Pull request #162 (2017-08-19 22:57:51)

Basic encrypted tensor
First prototype of an encrypted Tensor class PaillierTensor


Patch:
diff --git a/notebooks/Packed+Paillier.ipynb b/notebooks/Packed+Paillier.ipynb
new file mode 100644
index 0000000000..69e867d248
--- /dev/null
+++ b/notebooks/Packed+Paillier.ipynb
@@ -0,0 +1,257 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Packing multiple numbers"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import phe\n",
+    "import struct\n",
+    "import random"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "publickey, privatekey = phe.generate_paillier_keypair(n_length=2048)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Pack 4 byte numbers into a single number"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": [
+    "x = 20"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "800"
+      ]
+     },
+     "execution_count": 4,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "endian = 'little'\n",
+    "packed_fmt = \"x4p\" * x\n",
+    "struct.calcsize(packed_fmt)*8"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "def pack_numbers(numbers):\n",
+    "    p = []\n",
+    "    for n in numbers:\n",
+    "        n_bytes = n.to_bytes(4, endian)\n",
+    "        p.append(n_bytes)\n",
+    "        assert n == int.from_bytes(n_bytes, endian)\n",
+    "        #print(n_bytes)\n",
+    "\n",
+    "    encoded_bytes = struct.pack(packed_fmt, *p)\n",
+    "    encoded_num = int.from_bytes(encoded_bytes, endian)\n",
+    "    return encoded_num\n",
+    "\n",
+    "def unpack(encoded):\n",
+    "    enc_bytes = encoded.to_bytes(struct.calcsize(packed_fmt), endian)\n",
+    "    return [int.from_bytes(b, endian) for b in struct.unpack(packed_fmt, enc_bytes)]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "numbers_a = [random.randint(0, 2**20) for _ in range(x)]\n",
+    "\n",
+    "encoded_a = pack_numbers(numbers_a)\n",
+    "assert unpack(encoded_a) == numbers_a\n",
+    "\n",
+    "numbers_b = [random.randint(0, 2**20) for i in range(x)]\n",
+    "numbers_b[0] = 0\n",
+    "numbers_b[1] = 1\n",
+    "numbers_b[2] = 2**20\n",
+    "\n",
+    "encoded_b = pack_numbers(numbers_b)\n",
+    "assert unpack(encoded_b) == numbers_b"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 7,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": [
+    "ciphertext_a = publickey.raw_encrypt(encoded_a)\n",
+    "\n",
+    "ciphertext_b = publickey.raw_encrypt(encoded_b)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Now I assume that we can add them together.\n",
+    "\n",
+    "I'm not so confident about multiplying by a scalar:\n",
+    "\n",
+    "- E(a) * b"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 8,
+   "metadata": {
+    "scrolled": true
+   },
+   "outputs": [],
+   "source": [
+    "def compare(operation, res):\n",
+    "    for a, b, r in zip(numbers_a, numbers_b, res):\n",
+    "        opres = operation(a, b)\n",
+    "        assert opres == r\n",
+    "        print(opres, r)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Case 1\n",
+    "\n",
+    "$E(a) + E(b)$"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 9,
+   "metadata": {},
+   "outputs": [
+    {
+     "name": "stdout",
+     "output_type": "stream",
+     "text": [
+      "230434 230434\n",
+      "193515 193515\n",
+      "1438910 1438910\n",
+      "1359267 1359267\n",
+      "1515364 1515364\n",
+      "1319562 1319562\n",
+      "1125473 1125473\n",
+      "1641695 1641695\n",
+      "582305 582305\n",
+      "956673 956673\n",
+      "462229 462229\n",
+      "243885 243885\n",
+      "622645 622645\n",
+      "928660 928660\n",
+      "1251032 1251032\n",
+      "1593311 1593311\n",
+      "1466206 1466206\n",
+      "1041035 1041035\n",
+      "1734044 1734044\n",
+      "1588096 1588096\n"
+     ]
+    }
+   ],
+   "source": [
+    "ea = phe.EncryptedNumber(publickey, ciphertext_a)\n",
+    "cipher_both = ea._raw_add(ciphertext_a, ciphertext_b)\n",
+    "result = unpack(privatekey.raw_decrypt(cipher_both))\n",
+    "compare(lambda a,b: a+b, result)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 10,
+   "metadata": {},
+   "outputs": [
+    {
+     "ename": "OverflowError",
+     "evalue": "int too big to convert",
+     "output_type": "error",
+     "traceback": [
+      "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
+      "\u001b[0;31mOverflowError\u001b[0m                             Traceback (most recent call last)",
+      "\u001b[0;32m<ipython-input-10-3f2a4f3dd82e>\u001b[0m in \u001b[0;36m<module>\u001b[0;34m()\u001b[0m\n\u001b[1;32m      2\u001b[0m \u001b[0mea\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mphe\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mEncryptedNumber\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mpublickey\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mciphertext_a\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m      3\u001b[0m \u001b[0mcipher_both\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mea\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_raw_mul\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mencoded_b\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 4\u001b[0;31m \u001b[0mresult\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0munpack\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mprivatekey\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mraw_decrypt\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mcipher_both\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m      5\u001b[0m \u001b[0mcompare\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;32mlambda\u001b[0m \u001b[0ma\u001b[0m\u001b[0;34m,\u001b[0m\u001b[0mb\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0ma\u001b[0m\u001b[0;34m*\u001b[0m\u001b[0mb\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mresult\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
+      "\u001b[0;32m<ipython-input-5-2a2dbaadc88c>\u001b[0m in \u001b[0;36munpack\u001b[0;34m(encoded)\u001b[0m\n\u001b[1;32m     12\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m     13\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0munpack\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mencoded\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 14\u001b[0;31m     \u001b[0menc_bytes\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mencoded\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mto_bytes\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstruct\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mcalcsize\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mpacked_fmt\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mendian\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m     15\u001b[0m     \u001b[0;32mreturn\u001b[0m \u001b[0;34m[\u001b[0m\u001b[0mint\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mfrom_bytes\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mb\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mendian\u001b[0m\u001b[0;34m)\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mb\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mstruct\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0munpack\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mpacked_fmt\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0menc_bytes\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n",
+      "\u001b[0;31mOverflowError\u001b[0m: int too big to convert"
+     ]
+    }
+   ],
+   "source": [
+    "# Case 2 multiplying be a vector encoded number\n",
+    "ea = phe.EncryptedNumber(publickey, ciphertext_a)\n",
+    "cipher_both = ea._raw_mul(encoded_b)\n",
+    "result = unpack(privatekey.raw_decrypt(cipher_both))\n",
+    "compare(lambda a,b: a*b, result)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.6.1"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/notebooks/Syft - BaseTensor and PaillierTensor Playground.ipynb b/notebooks/Syft - BaseTensor and PaillierTensor Playground.ipynb
new file mode 100644
index 0000000000..39d48f8e58
--- /dev/null
+++ b/notebooks/Syft - BaseTensor and PaillierTensor Playground.ipynb
@@ -0,0 +1,300 @@
+{
+ "cells": [
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "from syft.he.paillier import KeyPair,PaillierTensor\n",
+    "from syft import TensorBase\n",
+    "import numpy as np"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "p,s = KeyPair().generate()\n",
+    "\n",
+    "x = PaillierTensor(p,np.array([1,2,3,4,5.]))\n",
+    "x2 = TensorBase(np.array([3,4,5,6,7.]))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 7,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "array([e, e, e, e, e], dtype=object)"
+      ]
+     },
+     "execution_count": 7,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "x + x2"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "True"
+      ]
+     },
+     "execution_count": 3,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "x2.encrypted"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 8,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "array([e, e, e, e, e], dtype=object)"
+      ]
+     },
+     "execution_count": 8,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "x2"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 13,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "out = s.decrypt(x2)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 19,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "array(array([  3.,   8.,  15.,  24.,  35.]), dtype=object)"
+      ]
+     },
+     "execution_count": 19,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "out.data"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "array(array([  3.,   8.,  15.,  24.,  35.]), dtype=object)"
+      ]
+     },
+     "execution_count": 6,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "x2.decrypt(s)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 29,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "True"
+      ]
+     },
+     "execution_count": 29,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": []
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 24,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "True"
+      ]
+     },
+     "execution_count": 24,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": []
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 17,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "215.0"
+      ]
+     },
+     "execution_count": 17,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "y.sum().decrypt(s)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 7,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "1"
+      ]
+     },
+     "execution_count": 7,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "x.dim()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "y= x2*x"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 28,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "True"
+      ]
+     },
+     "execution_count": 28,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "y.encrypted"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 29,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "out = y.sum()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 30,
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "55.0"
+      ]
+     },
+     "execution_count": 30,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "out.decrypt(s)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "collapsed": true
+   },
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.6.1"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
diff --git a/notebooks/Syft - Paillier Homomorphic Encryption Example.ipynb b/notebooks/Syft - Paillier Homomorphic Encryption Example.ipynb
index ef1b000d83..6a095c903f 100644
--- a/notebooks/Syft - Paillier Homomorphic Encryption Example.ipynb
+++ b/notebooks/Syft - Paillier Homomorphic Encryption Example.ipynb
@@ -13,7 +13,9 @@
   {
    "cell_type": "code",
    "execution_count": 1,
-   "metadata": {},
+   "metadata": {
+    "collapsed": true
+   },
    "outputs": [],
    "source": [
     "from syft.he.Paillier import KeyPair\n",
@@ -309,7 +311,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.5.3"
+   "version": "3.6.1"
   }
  },
  "nbformat": 4,
diff --git a/setup.py b/setup.py
index 48a4e06a3d..a2b5266cdd 100755
--- a/setup.py
+++ b/setup.py
@@ -1,5 +1,6 @@
 import os
-from setuptools import setup,find_packages
+from setuptools import setup, find_packages
+
 
 # Utility function to read the README file.
 # Used for the long_description.  It's nice, because now 1) we have a top level
@@ -8,17 +9,18 @@ from setuptools import setup,find_packages
 def read(fname):
     return open(os.path.join(os.path.dirname(__file__), fname)).read()
 
+
 requirements = read('requirements.txt').split()
 
 setup(
-    name = "syft",
-    version = "0.1.0",
-    author = "Amber Trask",
-    author_email = "contact@openmined.org",
-    description = ("A library for Homomorphically Encrypted Deep Learning Algorithms"),
-    license = "Apache-2.0",
-    keywords = "deep learning machine artificial intelligence homomorphic encryption",
-    packages=find_packages(exclude=['notebooks', 'test*','dist']),
+    name="syft",
+    version="0.1.0",
+    author="Amber Trask",
+    author_email="contact@openmined.org",
+    description=("A library for Encrypted Deep Learning Algorithms"),
+    license="Apache-2.0",
+    keywords="deep learning artificial intelligence homomorphic encryption",
+    packages=find_packages(exclude=['notebooks', 'test*', 'dist']),
     include_package_data=True,
     long_description=read('README.md'),
     url='github.com/OpenMined/Syft',
diff --git a/syft/__init__.py b/syft/__init__.py
index 3cbd2951f3..cacd7ba6fb 100644
--- a/syft/__init__.py
+++ b/syft/__init__.py
@@ -1,2 +1,13 @@
-from syft.tensor import *
-from syft.math import *
+from syft import he
+from syft import nn
+
+from syft.tensor import equal, TensorBase
+from syft.math import cumprod, cumsum, ceil, dot, matmul, addmm, addcmul
+from syft.math import addcdiv, addmv, addbmm, baddbmm
+
+s = str(he)
+s += str(nn)
+
+s += str(equal) + str(TensorBase) + str(cumprod) + str(cumsum) + str(ceil)
+s += str(dot) + str(matmul) + str(addmm) + str(addcmul) + str(addcdiv)
+s += str(addmv) + str(addbmm) + str(baddbmm)
diff --git a/syft/he/Paillier.py b/syft/he/Paillier.py
deleted file mode 100644
index 7d97ae3e97..0000000000
--- a/syft/he/Paillier.py
+++ /dev/null
@@ -1,128 +0,0 @@
-import phe as paillier
-import numpy as np
-import pickle
-
-class SecretKey():
-
-    def __init__(self,sk):
-        self.sk = sk
-
-    def decrypt(self,x):
-        """Decrypts x. X can be either an encrypted int or a numpy vector/matrix/tensor."""
-        if(type(x) == PaillierFloat):
-            return self.sk.decrypt(x)
-        elif(type(x) == np.ndarray):
-            sh = x.shape
-            x_ = x.reshape(-1)
-            out = list()
-            for v in x_:
-                out.append(self.sk.decrypt(v.data))
-            return np.array(out).reshape(sh)
-
-    def serialize(self):
-        return pickle.dumps(self.sk)
-
-class PublicKey():
-
-    def __init__(self,pk):
-        self.pk = pk
-
-    def encrypt(self,x):
-        """Encrypts x. X can be either an encrypted int or a numpy vector/matrix/tensor."""
-        if(type(x) == int):
-            return PaillierFloat(self,x)
-        elif(type(x) == np.ndarray):
-            sh = x.shape
-            x_ = x.reshape(-1)
-            out = list()
-            for v in x_:
-                out.append(PaillierFloat(self,v))
-            return np.array(out).reshape(sh)
-
-        else:
-            print("format not recognized")
-
-        return self.pk.encrypt(x)
-
-    def serialize(self):
-        return pickle.dumps(self.pk)
-
-class KeyPair():
-
-    def __init__(self):
-        ""
-
-    def deserialize(self,pubkey,seckey):
-        self.public_key = PublicKey(pickle.loads(pubkey))
-        self.secret_key = SecretKey(pickle.loads(seckey))
-        return (self.public_key, self.secret_key)
-
-    def generate(self,n_length=1024):
-        pubkey, prikey = paillier.generate_paillier_keypair(n_length=n_length)
-        self.public_key = PublicKey(pubkey)
-        self.secret_key = SecretKey(prikey)
-
-        return (self.public_key,self.secret_key)
-
-class PaillierFloat():
-
-    def __init__(self,public_key,data=None):
-        """Wraps pointer to encrypted integer with an interface that numpy can use."""
-
-        self.public_key = public_key
-        if(data is not None):
-            self.data = self.public_key.pk.encrypt(data)
-        else:
-            self.data = None
-
-    def __add__(self,y):
-        """Adds two encrypted integers together."""
-
-        out = PaillierFloat(self.public_key,None)
-        out.data = self.data + y.data
-        return out
-
-    def __sub__(self,y):
-        """Subtracts two encrypted integers."""
-
-        out = PaillierFloat(self.public_key, None)
-        out.data = self.data - y.data
-        return out
-
-    def __mul__(self,y):
-        """Multiplies two integers. y may be encrypted or a simple integer."""
-
-        if(type(y) == type(self)):
-            out = PaillierFloat(self.public_key, None)
-            out.data = self.data * y.data
-            return out
-        elif(type(y) == int or type(y) == float):
-            out = PaillierFloat(self.public_key, None)
-            out.data = self.data * y
-            return out
-        else:
-            return None
-
-    def __truediv__(self,y):
-        """Divides two integers. y may be encrypted or a simple integer."""
-
-        if(type(y) == type(self)):
-            out = PaillierFloat(self.public_key, None)
-            out.data = self.data / y.data
-            return out
-        elif(type(y) == int):
-            out = PaillierFloat(self.public_key, None)
-            out.data = self.data / y
-            return out
-        else:
-            return None
-
-    def __repr__(self):
-        """This is kindof a boring/uninformative __repr__"""
-
-        return 'e'
-
-    def __str__(self):
-        """This is kindof a boring/uninformative __str__"""
-
-        return 'e'
diff --git a/syft/he/__init__.py b/syft/he/__init__.py
index e69de29bb2..f626644aa1 100644
--- a/syft/he/__init__.py
+++ b/syft/he/__init__.py
@@ -0,0 +1,5 @@
+from . import paillier
+from . import abstract
+
+s = str(paillier)
+s += str(abstract)
diff --git a/syft/he/abstract/__init__.py b/syft/he/abstract/__init__.py
new file mode 100644
index 0000000000..92cf078721
--- /dev/null
+++ b/syft/he/abstract/__init__.py
@@ -0,0 +1,4 @@
+from . import keys
+
+s = str(keys)
+s += "asdf"
diff --git a/syft/he/abstract/keys.py b/syft/he/abstract/keys.py
new file mode 100644
index 0000000000..3afadfab7c
--- /dev/null
+++ b/syft/he/abstract/keys.py
@@ -0,0 +1,34 @@
+class AbstractSecretKey():
+
+    def __init__(self, sk):
+        return NotImplemented
+
+    def decrypt(self, x):
+        return NotImplemented
+
+    def serialize(self):
+        return NotImplemented
+
+
+class AbstractPublicKey():
+
+    def __init__(self, pk):
+        return NotImplemented
+
+    def encrypt(self, x):
+        return NotImplemented
+
+    def serialize(self):
+        return NotImplemented
+
+
+class AbstractKeyPair():
+
+    def __init__(self):
+        ""
+
+    def deserialize(self, pubkey, seckey):
+        return NotImplemented
+
+    def generate(self, n_length=1024):
+        return NotImplemented
diff --git a/syft/he/paillier/__init__.py b/syft/he/paillier/__init__.py
new file mode 100644
index 0000000000..8a1f99603f
--- /dev/null
+++ b/syft/he/paillier/__init__.py
@@ -0,0 +1,5 @@
+from .basic import PaillierTensor, Float
+from .keys import SecretKey, PublicKey, KeyPair
+
+s = str(PaillierTensor) + str(Float) + str(SecretKey) + str(PublicKey)
+s += str(KeyPair)
diff --git a/syft/he/paillier/basic.py b/syft/he/paillier/basic.py
new file mode 100644
index 0000000000..2720d24919
--- /dev/null
+++ b/syft/he/paillier/basic.py
@@ -0,0 +1,118 @@
+import numpy as np
+from ...tensor import TensorBase
+
+
+class PaillierTensor(TensorBase):
+
+    def __init__(self, public_key, data=None, input_is_decrypted=True):
+        self.encrypted = True
+
+        self.public_key = public_key
+        if(type(data) == np.ndarray and input_is_decrypted):
+            self.data = public_key.encrypt(data, True)
+        else:
+            self.data = data
+
+    def __add__(self, tensor):
+        """Performs element-wise addition between two tensors"""
+
+        if(not isinstance(tensor, TensorBase)):
+            # try encrypting it
+            tensor = self.public_key.encrypt(tensor)
+            return PaillierTensor(self.public_key, self.data + tensor, False)
+
+        if(type(tensor) == TensorBase):
+            tensor = PaillierTensor(self.public_key, tensor.data)
+
+        return PaillierTensor(self.public_key, self.data + tensor.data, False)
+
+    def __mul__(self, tensor):
+        """Performs element-wise addition between two tensors"""
+
+        if(isinstance(tensor, TensorBase)):
+            if(not tensor.encrypted):
+                result = self.data * tensor.data
+                o = PaillierTensor(self.public_key, result, False)
+                return o
+            else:
+                return NotImplemented
+        else:
+            return PaillierTensor(self.public_key, self.data * tensor, False)
+
+    def sum(self, dim=None):
+        """Returns the sum of all elements in the input array."""
+        if not self.encrypted:
+            return NotImplemented
+
+        if dim is None:
+            return self.data.sum()
+        else:
+            return self.data.sum(axis=dim)
+
+
+class Float():
+
+    def __init__(self, public_key, data=None):
+        """Wraps pointer to encrypted Float with an interface that numpy
+        can use."""
+
+        self.public_key = public_key
+        if(data is not None):
+            self.data = self.public_key.pk.encrypt(data)
+        else:
+            self.data = None
+
+    def decrypt(self, secret_key):
+        return secret_key.decrypt(self)
+
+    def __add__(self, y):
+        """Adds two encrypted Floats together."""
+
+        out = Float(self.public_key, None)
+        out.data = self.data + y.data
+        return out
+
+    def __sub__(self, y):
+        """Subtracts two encrypted Floats."""
+
+        out = Float(self.public_key, None)
+        out.data = self.data - y.data
+        return out
+
+    def __mul__(self, y):
+        """Multiplies two Floats. y may be encrypted or a simple Float."""
+
+        if(type(y) == type(self)):
+            out = Float(self.public_key, None)
+            out.data = self.data * y.data
+            return out
+        elif(type(y) == int or type(y) == float):
+            out = Float(self.public_key, None)
+            out.data = self.data * y
+            return out
+        else:
+            return None
+
+    def __truediv__(self, y):
+        """Divides two Floats. y may be encrypted or a simple Float."""
+
+        if(type(y) == type(self)):
+            out = Float(self.public_key, None)
+            out.data = self.data / y.data
+            return out
+        elif(type(y) == int):
+            out = Float(self.public_key, None)
+            out.data = self.data / y
+            return out
+        else:
+            return None
+
+    def __repr__(self):
+        """This is kindof a boring/uninformative __repr__"""
+
+        return 'e'
+
+    def __str__(self):
+        """This is kindof a boring/uninformative __str__"""
+
+        return 'e'
diff --git a/syft/he/paillier/keys.py b/syft/he/paillier/keys.py
new file mode 100644
index 0000000000..20eeb20e8c
--- /dev/null
+++ b/syft/he/paillier/keys.py
@@ -0,0 +1,91 @@
+import phe as paillier
+import numpy as np
+import pickle
+from .basic import Float, PaillierTensor
+from ...tensor import TensorBase
+from ..abstract.keys import AbstractSecretKey, AbstractPublicKey
+from ..abstract.keys import AbstractKeyPair
+
+
+class SecretKey(AbstractSecretKey):
+
+    def __init__(self, sk):
+        self.sk = sk
+
+    def decrypt(self, x):
+        """Decrypts x. X can be either an encrypted int or a numpy
+        vector/matrix/tensor."""
+
+        if(type(x) == Float):
+            return self.sk.decrypt(x.data)
+        elif(type(x) == TensorBase or type(x) == PaillierTensor):
+            if(x.encrypted):
+                return TensorBase(self.decrypt(x.data), encrypted=False)
+            else:
+                return NotImplemented
+        elif(type(x) == np.ndarray):
+            sh = x.shape
+            x_ = x.reshape(-1)
+            out = list()
+            for v in x_:
+                out.append(self.sk.decrypt(v.data))
+            return np.array(out).reshape(sh)
+        else:
+            return NotImplemented
+
+    def serialize(self):
+        return pickle.dumps(self.sk)
+
+
+class PublicKey(AbstractPublicKey):
+
+    def __init__(self, pk):
+        self.pk = pk
+
+    def encrypt(self, x, same_type=False):
+        """Encrypts x. X can be either an encrypted int or a numpy
+        vector/matrix/tensor."""
+        if(type(x) == int):
+            if(same_type):
+                return NotImplemented
+            return Float(self, x)
+        elif(type(x) == TensorBase):
+            if(x.encrypted or same_type):
+                return NotImplemented
+            return PaillierTensor(self, x.data)
+        elif(type(x) == np.ndarray):
+            sh = x.shape
+            x_ = x.reshape(-1)
+            out = list()
+            for v in x_:
+                out.append(Float(self, v))
+            if(same_type):
+                return np.array(out).reshape(sh)
+            else:
+                return PaillierTensor(self, np.array(out).reshape(sh))
+        else:
+            print("format not recognized")
+            return NotImplemented
+
+        return self.pk.encrypt(x)
+
+    def serialize(self):
+        return pickle.dumps(self.pk)
+
+
+class KeyPair(AbstractKeyPair):
+
+    def __init__(self):
+        ""
+
+    def deserialize(self, pubkey, seckey):
+        self.public_key = PublicKey(pickle.loads(pubkey))
+        self.secret_key = SecretKey(pickle.loads(seckey))
+        return (self.public_key, self.secret_key)
+
+    def generate(self, n_length=1024):
+        pubkey, prikey = paillier.generate_paillier_keypair(n_length=n_length)
+        self.public_key = PublicKey(pubkey)
+        self.secret_key = SecretKey(prikey)
+
+        return (self.public_key, self.secret_key)
diff --git a/syft/math.py b/syft/math.py
index c4eb9648d8..e2df887156 100644
--- a/syft/math.py
+++ b/syft/math.py
@@ -5,15 +5,16 @@ from .tensor import _ensure_tensorbase
 
 __all__ = [
 
-    'cumprod','cumsum','ceil','dot', 'matmul','addmm','addcmul','addcdiv',
-    'addmv','addbmm','baddbmm',
+    'cumprod', 'cumsum', 'ceil', 'dot', 'matmul', 'addmm', 'addcmul',
+    'addcdiv', 'addmv', 'addbmm', 'baddbmm',
 ]
 
 
 def dot(tensor1, tensor2):
     """Returns inner product of two tensors.
 
-    N-dimensional tensors are flattened into 1-D vectors, therefore this method should only be used on vectors.
+    N-dimensional tensors are flattened into 1-D vectors, therefore this
+    method should only be used on vectors.
     """
 
     tensor1 = _ensure_tensorbase(tensor1)
@@ -29,12 +30,19 @@ def matmul(tensor1, tensor2):
 
     Exact behavior depends on the input tensors' dimensionality like so:
     * If both tensors are 1-dimensional, their dot product is returned.
-    * If both tensors are 2-dimensional, their matrix-matrix product is returned.
-    * If either tensor has dimensionality > 2, the last 2 dimensions are treated as matrices and multiplied.
-    * If tensor1 is 1-dimensional, it is converted to a matrix by prepending a 1 to its dimensions.
-      This prepended dimension is removed after the matrix multiplication.
-    * If tensor2 is 1-dimensional, it is converted to a matrix by prepending a 1 to its dimensions.
-      This prepended dimension is removed after the matrix multiplication.
+    * If both tensors are 2-dimensional, their matrix-matrix product is
+    returned.
+
+    * If either tensor has dimensionality > 2, the last 2 dimensions are
+    treated as matrices and multiplied.
+
+    * If tensor1 is 1-dimensional, it is converted to a matrix by prepending
+    a 1 to its dimensions. This prepended dimension is removed after the
+    matrix multiplication.
+
+    * If tensor2 is 1-dimensional, it is converted to a matrix by prepending
+    a 1 to its dimensions. This prepended dimension is removed after the
+    matrix multiplication.
     """
 
     tensor1 = _ensure_tensorbase(tensor1)
@@ -68,7 +76,6 @@ def ceil(tensor):
     return TensorBase(np.ceil(tensor.data))
 
 
-
 def cumsum(tensor, dim=0):
     """
     Returns the cumulative sum of the elements along a given dimension
@@ -103,13 +110,25 @@ def cumprod(tensor, dim=0):
 
 
 def addmm(tensor1, tensor2, mat, beta=1, alpha=1):
-        """Performs ((Mat*Beta)+((Tensor1.Tensor2)*Alpha)) and  returns the result as a Tensor
-            Tensor1.Tensor2 is performed as Matrix product of two array The behavior depends on the arguments in the following way.
+        """Performs ((Mat*Beta)+((Tensor1.Tensor2)*Alpha)) and  returns the
+        result as a Tensor
+            Tensor1.Tensor2 is performed as Matrix product of two array
+            The behavior depends on the arguments in the following way.
             *If both tensors are 1-dimensional, their dot product is returned.
-            *If both arguments are 2-D they are multiplied like conventional matrices.
-            *If either argument is N-D, N > 2, it is treated as a stack of matrices residing in the last two indexes and broadcast accordingly.
-            *If the first argument is 1-D, it is promoted to a matrix by prepending a 1 to its dimensions. After matrix multiplication the prepended 1 is removed.
-            *If the second argument is 1-D, it is promoted to a matrix by appending a 1 to its dimensions. After matrix multiplication the appended 1 is removed.
+            *If both arguments are 2-D they are multiplied like conventional
+            matrices.
+
+            *If either argument is N-D, N > 2, it is treated as a stack of
+            matrices residing in the last two indexes and broadcast
+            accordingly.
+
+            *If the first argument is 1-D, it is promoted to a matrix by
+            prepending a 1 to its dimensions. After matrix multiplication
+            the prepended 1 is removed.
+
+            *If the second argument is 1-D, it is promoted to a matrix by
+            appending a 1 to its dimensions. After matrix multiplication
+            the appended 1 is removed.
             """
         _ensure_tensorbase(tensor1)
         _ensure_tensorbase(tensor2)
@@ -117,36 +136,40 @@ def addmm(tensor1, tensor2, mat, beta=1, alpha=1):
         if tensor1.encrypted or tensor2.encrypted or mat.encrypted:
             return NotImplemented
         else:
-            return TensorBase(np.array(((mat.data)*beta)+((np.matmul(tensor1.data, tensor2.data))*alpha)))
+            delta = (np.matmul(tensor1.data, tensor2.data))
+            return TensorBase(np.array(((mat.data) * beta) + (delta * alpha)))
 
 
 def addcmul(tensor1, tensor2, mat, value=1):
 
-    """Performs the element-wise multiplication of tensor1 by tensor2,  multiply the result by the scalar value and add it to mat."""
+    """Performs the element-wise multiplication of tensor1 by tensor2,
+    multiply the result by the scalar value and add it to mat."""
     _ensure_tensorbase(tensor1)
     _ensure_tensorbase(tensor2)
     _ensure_tensorbase(mat)
     if tensor1.encrypted or tensor2.encrypted or mat.encrypted:
         return NotImplemented
     else:
-        out = (mat.data)+((tensor1.data*tensor2.data)*value)
+        out = (mat.data) + ((tensor1.data * tensor2.data) * value)
         return TensorBase(out)
 
 
 def addcdiv(tensor1, tensor2, mat, value=1):
-    """Performs the element-wise division of tensor1 by tensor2,  multiply the result by the scalar value and add it to mat."""
+    """Performs the element-wise division of tensor1 by tensor2, multiply
+    the result by the scalar value and add it to mat."""
     _ensure_tensorbase(tensor1)
     _ensure_tensorbase(tensor2)
     _ensure_tensorbase(mat)
     if tensor1.encrypted or tensor2.encrypted or mat.encrypted:
         return NotImplemented
     else:
-        out = (mat.data)+((tensor1.data/tensor2.data)*value)
+        out = (mat.data) + ((tensor1.data / tensor2.data) * value)
         return TensorBase(out)
 
 
 def addmv(tensor1, mat, vec, beta=1, alpha=1):
-    """"Performs a matrix-vector product of the matrix mat and the vector vec. The vector tensor is added to the final result.
+    """"Performs a matrix-vector product of the matrix mat and the vector vec.
+    The vector tensor is added to the final result.
           tensor1 and vec are 1d tensors
           out=(beta∗tensor)+(alpha∗(mat@vec2))"""
     _ensure_tensorbase(tensor1)
@@ -159,16 +182,19 @@ def addmv(tensor1, mat, vec, beta=1, alpha=1):
     elif tensor1.encrypted or vec.encrypted or mat.encrypted:
         return NotImplemented
     else:
-        out = (tensor1.data*beta)+(np.matmul(mat.data, vec.data)*alpha)
+        out = (tensor1.data * beta) + (np.matmul(mat.data, vec.data) * alpha)
         return TensorBase(out)
 
 
 def addbmm(tensor1, tensor2, mat, beta=1, alpha=1):
-    """Performs a batch matrix-matrix product of matrices stored in batch1(tensor1) and batch2(tensor2),
-     with a reduced add step (all matrix multiplications get accumulated along the first dimension).
+    """Performs a batch matrix-matrix product of matrices stored in
+    batch1(tensor1) and batch2(tensor2),
+     with a reduced add step (all matrix multiplications get accumulated along
+     the first dimension).
      mat is added to the final result.
      res=(beta∗M)+(alpha∗sum(batch1i@batch2i, i=0, b))
-    * batch1 and batch2 must be 3D Tensors each containing the same number of matrices."""
+    * batch1 and batch2 must be 3D Tensors each containing the same number of
+    matrices."""
     _ensure_tensorbase(tensor1)
     _ensure_tensorbase(tensor2)
     _ensure_tensorbase(mat)
@@ -188,9 +214,11 @@ def addbmm(tensor1, tensor2, mat, beta=1, alpha=1):
 
 
 def baddbmm(tensor1, tensor2, mat, beta=1, alpha=1):
-    """Performs a batch matrix-matrix product of matrices in batch1(tensor1) and batch2(tensor2). mat is added to the final result.
+    """Performs a batch matrix-matrix product of matrices in batch1(tensor1)
+    and batch2(tensor2). mat is added to the final result.
       resi=(beta∗Mi)+(alpha∗batch1i×batch2i)
-      *batch1 and batch2 must be 3D Tensors each containing the same number of matrices."""
+      *batch1 and batch2 must be 3D Tensors each containing the same number
+      of matrices."""
     _ensure_tensorbase(tensor1)
     _ensure_tensorbase(tensor2)
     _ensure_tensorbase(mat)
@@ -204,5 +232,5 @@ def baddbmm(tensor1, tensor2, mat, beta=1, alpha=1):
         return NotImplemented
     else:
         mm = np.matmul(tensor1.data, tensor2.data)
-        out = (mat.data*beta)+(mm*alpha)
+        out = (mat.data * beta) + (mm * alpha)
         return TensorBase(out)
diff --git a/syft/nn/__init__.py b/syft/nn/__init__.py
index e69de29bb2..79a39671f6 100644
--- a/syft/nn/__init__.py
+++ b/syft/nn/__init__.py
@@ -0,0 +1,3 @@
+from . import linear
+n = linear.LinearClassifier()
+n.weights = list()
diff --git a/syft/nn/convnet.py b/syft/nn/convnet.py
index 0ec140be30..8783cfecc1 100644
--- a/syft/nn/convnet.py
+++ b/syft/nn/convnet.py
@@ -1,1 +1,1 @@
-#TODO: build this
+# TODO: build this
diff --git a/syft/nn/fasttext.py b/syft/nn/fasttext.py
index 0ec140be30..8783cfecc1 100644
--- a/syft/nn/fasttext.py
+++ b/syft/nn/fasttext.py
@@ -1,1 +1,1 @@
-#TODO: build this
+# TODO: build this
diff --git a/syft/nn/linear.py b/syft/nn/linear.py
index 11bdd034f3..85e326914f 100644
--- a/syft/nn/linear.py
+++ b/syft/nn/linear.py
@@ -1,20 +1,20 @@
-from syft import he
 import numpy as np
 
+
 class LinearClassifier():
-    """This class is a basic linear classifier with functionality to encrypt/decrypt
-    weights according to any of the homomorphic encryption schemes in syft.he. It also
-    contains the logic to make predictions when in an encrypted state.
+    """This class is a basic linear classifier with functionality to
+    encrypt/decrypt weights according to any of the homomorphic encryption
+    schemes in syft.he. It also contains the logic to make predictions when
+    in an encrypted state.
 
     TODO: create a generic interface for ML models that this class implements.
 
     TODO: minibatching in the "learn" API. The greater the batch size, the more
-    iterations should be doable before the homomorphic encryption noise grows too
-    much to be decrypted.
+    iterations should be doable before the homomorphic encryption noise grows
+    too much to be decrypted.
     """
 
-
-    def __init__(self,n_inputs=4,n_labels=2,desc=""):
+    def __init__(self, n_inputs=4, n_labels=2, desc=""):
 
         self.desc = desc
 
@@ -28,7 +28,7 @@ class LinearClassifier():
         self.pubkey = None
         self.encrypted = False
 
-    def encrypt(self,pubkey):
+    def encrypt(self, pubkey):
         """iterates through each weight and encrypts it
 
         TODO: check that weights are actually decrypted
@@ -37,42 +37,42 @@ class LinearClassifier():
 
         self.encrypted = True
         self.pubkey = pubkey
-        for i,w in enumerate(self.weights):
+        for i, w in enumerate(self.weights):
             self.weights[i] = self.pubkey.encrypt(w)
         return self
 
-    def decrypt(self,seckey):
+    def decrypt(self, seckey):
         """iterates through each weight and decrypts it
 
         TODO: check that weights are actually encrypted
 
         """
         self.encrypted = False
-        for i,w in enumerate(self.weights):
+        for i, w in enumerate(self.weights):
             self.weights[i] = seckey.decrypt(w)
         return self
 
-    def forward(self,input):
+    def forward(self, input):
 
         """Makes a prediction based on input. If the network is encrypted, the
         prediction is also encrypted and vise versa."""
 
         pred = self.weights[0] * input[0]
-        for j,each_inp in enumerate(input[1:]):
+        for j, each_inp in enumerate(input[1:]):
             if(each_inp == 1):
-                pred = pred + self.weights[j+1]
+                pred = pred + self.weights[j + 1]
             elif(each_inp != 0):
-                pred = pred + (self.weights[j+1] * input[j+1])
+                pred = pred + (self.weights[j + 1] * input[j + 1])
 
         return pred
 
-    def generate_gradient(self,input,target):
+    def generate_gradient(self, input, target):
         target = np.array(target).astype('float64')
         pred = self.forward(input)
 
         target_v = target
 
-        if(self.pubkey is not None and self.encrypted == True):
+        if(self.pubkey is not None and self.encrypted):
             target_v = self.pubkey.encrypt(target_v)
 
         output_grad = (pred - target_v)
@@ -92,11 +92,10 @@ class LinearClassifier():
 
         return weight_grad
 
-
-    def learn(self,input,target,alpha=0.5):
+    def learn(self, input, target, alpha=0.5):
         """Updates weights based on input and target prediction. Note, updating
-        weights increases the noise in the encrypted weights and will eventually
-        require the weights to be re-encrypted.
+        weights increases the noise in the encrypted weights and will
+        eventually require the weights to be re-encrypted.
 
         TODO: minibatching
         TODO: instead of storing weights, store aggregated weight updates (and
@@ -104,31 +103,31 @@ class LinearClassifier():
 
         """
 
-        weight_update = self.generate_gradient(input,target)
+        weight_update = self.generate_gradient(input, target)
         self.weights -= weight_update * alpha
 
         return weight_update
 
-    def evaluate(self,inputs,targets):
-        """accepts a list of inputs and a list of targets - returns the mean squared
-        error scaled by a fixed amount and converted to an integer."""
+    def evaluate(self, inputs, targets):
+        """accepts a list of inputs and a list of targets - returns the mean
+        squared error scaled by a fixed amount and converted to an integer."""
 
         scale = 1000
 
-        if(self.encrypted == True):
+        if(self.encrypted):
             return "not yet supported... but will in the future"
         else:
 
             loss = 0
-            for i,row in enumerate(inputs):
+            for i, row in enumerate(inputs):
                 pred = self.forward(row)
                 true = targets[i]
                 loss += (pred - true)**2
-            return int((loss[0]*scale)/float(len(inputs)))
-
+            return int((loss[0] * scale) / float(len(inputs)))
 
     def __str__(self):
-        return "Linear Model ("+str(self.n_inputs)+","+str(self.n_labels)+"): " + str(self.desc)
+        left = "Linear Model (" + str(self.n_inputs) + ","
+        return left + str(self.n_labels) + "): " + str(self.desc)
 
     def __repr__(self):
         return self.__str__()
diff --git a/syft/nn/rl.py b/syft/nn/rl.py
index 2708f54e00..8783cfecc1 100644
--- a/syft/nn/rl.py
+++ b/syft/nn/rl.py
@@ -1,1 +1,1 @@
-# TODO: build this 
+# TODO: build this
diff --git a/syft/nn/word2vec.py b/syft/nn/word2vec.py
index 82c873265f..b1d67f003e 100644
--- a/syft/nn/word2vec.py
+++ b/syft/nn/word2vec.py
@@ -1,1 +1,1 @@
-#TOOD: build this
+# TOOD: build this
diff --git a/syft/tensor.py b/syft/tensor.py
index cb1759d541..9c9af98c9a 100644
--- a/syft/tensor.py
+++ b/syft/tensor.py
@@ -37,19 +37,36 @@ def equal(tensor1, tensor2):
     if tensor1.encrypted or tensor2.encrypted:
         return NotImplemented
 
-    return tensor1.data.shape == tensor2.data.shape and np.allclose(tensor1.data, tensor2.data)
+    left = tensor1.data.shape == tensor2.data.shape
+    right = np.allclose(tensor1.data, tensor2.data)
+    return left and right
 
 
 class TensorBase(object):
     """
     A base tensor class that performs basic element-wise operation such as
-    addition, subtraction, multiplication and division, and also dot and matrix products.
+    addition, subtraction, multiplication and division, and also dot and
+    matrix products.
     """
 
     def __init__(self, arr_like, encrypted=False):
         self.data = _ensure_ndarray(arr_like)
         self.encrypted = encrypted
 
+    def encrypt(self, pubkey):
+        """Encrypts the Tensor using a Public Key"""
+        if(self.encrypted):
+            return NotImplemented
+        else:
+            return pubkey.encrypt(self)
+
+    def decrypt(self, seckey):
+        """Decrypts the tensor using a Secret Key"""
+        if(self.encrypted):
+            return seckey.decrypt(self)
+        else:
+            return self
+
     def __add__(self, tensor):
         """Performs element-wise addition between two tensors"""
         if self.encrypted:
@@ -107,19 +124,29 @@ class TensorBase(object):
 
     def __mul__(self, tensor):
         """Performs element-wise multiplication between two tensors"""
+
         if self.encrypted:
             return NotImplemented
 
-        tensor = _ensure_tensorbase(tensor)
-        return TensorBase(self.data * tensor.data)
+        # if it's a sub-class of TensorBase, use the multiplication of that
+        # subclass not this one.
+        if(type(tensor) != TensorBase and isinstance(tensor, TensorBase)):
+            return tensor * self
+        else:
+            tensor = _ensure_tensorbase(tensor)
+            return TensorBase(tensor.data * self.data)
 
     def __imul__(self, tensor):
         """Performs in place element-wise multiplication between two tensors"""
         if self.encrypted:
             return NotImplemented
 
-        tensor = _ensure_tensorbase(tensor)
-        self.data *= tensor.data
+        if(type(tensor) != TensorBase and isinstance(tensor, TensorBase)):
+            self.data = tensor.data * self.data
+            self.encrypted = tensor.encrypted
+        else:
+            tensor = _ensure_tensorbase(tensor)
+            self.data *= tensor.data
         return self
 
     def __truediv__(self, tensor):
@@ -172,8 +199,6 @@ class TensorBase(object):
 
     def dim(self):
         """Returns an integer of the number of dimensions of this tensor."""
-        if self.encrypted:
-            return NotImplemented
 
         return self.data.ndim
 
@@ -194,24 +219,49 @@ class TensorBase(object):
         return np.ceil(self.data)
 
     def addmm(self, tensor2, mat, beta=1, alpha=1):
-        """Performs ((Mat*Beta)+((Tensor1@Tensor2)*Alpha)) and  returns the result as a Tensor
-            Tensor1.Tensor2 is performed as Matrix product of two array The behavior depends on the arguments in the following way.
+        """Performs ((Mat*Beta)+((Tensor1@Tensor2)*Alpha)) and  returns the
+        result as a Tensor
+            Tensor1.Tensor2 is performed as Matrix product of two array The
+            behavior depends on the arguments in the following way.
             *If both tensors are 1-dimensional, their dot product is returned.
-            *If both arguments are 2-D they are multiplied like conventional matrices.
-            *If either argument is N-D, N > 2, it is treated as a stack of matrices residing in the last two indexes and broadcast accordingly.
-            *If the first argument is 1-D, it is promoted to a matrix by prepending a 1 to its dimensions. After matrix multiplication the prepended 1 is removed.
-            *If the second argument is 1-D, it is promoted to a matrix by appending a 1 to its dimensions. After matrix multiplication the appended 1 is removed.
+            *If both arguments are 2-D they are multiplied like conventional
+            matrices.
+
+            *If either argument is N-D, N > 2, it is treated as a stack of
+            matrices residing in the last two indexes and broadcast
+            accordingly.
+
+            *If the first argument is 1-D, it is promoted to a matrix by
+            prepending a 1 to its dimensions. After matrix multiplication the
+            prepended 1 is removed.
+            *If the second argument is 1-D, it is promoted to a matrix by
+            appending a 1 to its dimensions. After matrix multiplication the
+            appended 1 is removed.
             """
         return syft.addmm(self, tensor2, mat, beta, alpha)
 
     def addmm_(self, tensor2, mat, beta=1, alpha=1):
-        """Performs ((Mat*Beta)+((Tensor1@Tensor2)*Alpha)) and updates Tensor1 with result and reurns it
-            Tensor1.Tensor2 is performed as Matrix product of two array The behavior depends on the arguments in the following way.
-            *If both tensors are 1-dimensional,  their dot product is returned.
-            *If both arguments are 2-D they are multiplied like conventional matrices.
-            *If either argument is N-D,  N > 2,  it is treated as a stack of matrices residing in the last two indexes and broadcast accordingly.
-            *If the first argument is 1-D,  it is promoted to a matrix by prepending a 1 to its dimensions. After matrix multiplication the prepended 1 is removed.
-            *If the second argument is 1-D,  it is promoted to a matrix by appending a 1 to its dimensions. After matrix multiplication the appended 1 is removed.
+        """Performs ((Mat*Beta)+((Tensor1@Tensor2)*Alpha)) and updates Tensor1
+        with result and reurns it
+            Tensor1.Tensor2 is performed as Matrix product of two array The
+            behavior depends on the arguments in the following way.
+
+            *If both tensors are 1-dimensional, their dot product is returned.
+
+            *If both arguments are 2-D they are multiplied like conventional
+            matrices.
+
+            *If either argument is N-D, N > 2, it is treated as a stack of
+            matrices residing in the last two indexes and broadcast
+            accordingly.
+
+            *If the first argument is 1-D, it is promoted to a matrix by
+            prepending a 1 to its dimensions. After matrix multiplication the
+            prepended 1 is removed.
+
+            *If the second argument is 1-D, it is promoted to a matrix by
+            appending a 1 to its dimensions. After matrix multiplication the
+            appended 1 is removed.
             """
         _ensure_tensorbase(tensor2)
         _ensure_tensorbase(mat)
@@ -221,15 +271,17 @@ class TensorBase(object):
             self.data = np.array((np.matmul(self.data, tensor2.data)))
             self.data *= alpha
             mat.data *= beta
-            self.data = self.data+mat.data
+            self.data = self.data + mat.data
             return self
 
     def addcmul(self, tensor2, mat, value=1):
-        """Performs the element-wise multiplication of tensor1 by tensor2,  multiply the result by the scalar value and add it to mat."""
+        """Performs the element-wise multiplication of tensor1 by tensor2,
+        multiply the result by the scalar value and add it to mat."""
         return syft.addcmul(self, tensor2, mat, value)
 
     def addcmul_(self, tensor2, mat, value=1):
-        """Performs implace element-wise multiplication of tensor1 by tensor2,  multiply the result by the scalar value and add it to mat."""
+        """Performs implace element-wise multiplication of tensor1 by tensor2,
+        multiply the result by the scalar value and add it to mat."""
         _ensure_tensorbase(tensor2)
         _ensure_tensorbase(mat)
         if self.encrypted or tensor2.encrypted or mat.encrypted:
@@ -241,29 +293,33 @@ class TensorBase(object):
             return self
 
     def addcdiv(self, tensor2, mat, value=1):
-        """Performs the element-wise division of tensor1 by tensor2,  multiply the result by the scalar value and add it to mat."""
+        """Performs the element-wise division of tensor1 by tensor2,
+        multiply the result by the scalar value and add it to mat."""
         return syft.addcdiv(self, tensor2, mat, value)
 
     def addcdiv_(self, tensor2, mat, value=1):
-        """Performs implace element-wise division of tensor1 by tensor2,  multiply the result by the scalar value and add it to mat."""
+        """Performs implace element-wise division of tensor1 by tensor2,
+        multiply the result by the scalar value and add it to mat."""
         _ensure_tensorbase(tensor2)
         _ensure_tensorbase(mat)
         if self.encrypted or tensor2.encrypted or mat.encrypted:
             return NotImplemented
         else:
-            self.data = self.data/tensor2.data
+            self.data = self.data / tensor2.data
             self.data *= value
             self.data += mat.data
             return self
 
     def addmv(self, mat, vec, beta=1, alpha=1):
-        """"Performs a matrix-vector product of the matrix mat and the vector vec. The vector tensor is added to the final result.
+        """"Performs a matrix-vector product of the matrix mat and the vector
+         vec. The vector tensor is added to the final result.
               tensor1 and vec are 1d tensors
               out=(beta∗tensor)+(alpha∗(mat@vec2))"""
         return syft.addmv(self, mat, vec, beta, alpha)
 
     def addmv_(self, mat, vec, beta=1, alpha=1):
-        """"Performs a inplace matrix-vector product of the matrix mat and the vector vec. The vector tensor is added to the final result.
+        """"Performs a inplace matrix-vector product of the matrix mat and the
+         vector vec. The vector tensor is added to the final result.
               tensor1 and vec are 1d tensors
               out=(beta∗tensor)+(alpha∗(mat@vec2))"""
         _ensure_tensorbase(vec)
@@ -276,25 +332,28 @@ class TensorBase(object):
             return NotImplemented
         else:
             self *= beta
-            temp = np.matmul(mat.data, vec.data)*alpha
+            temp = np.matmul(mat.data, vec.data) * alpha
             self += temp
             return self
 
     def addbmm(self, tensor2, mat, beta=1, alpha=1):
-        """Performs a batch matrix-matrix product of matrices stored in batch1(tensor1) and batch2(tensor2),
-         with a reduced add step (all matrix multiplications get accumulated along the first dimension).
+        """Performs a batch matrix-matrix product of matrices stored in
+        batch1(tensor1) and batch2(tensor2), with a reduced add step (all
+        matrix multiplications get accumulated along the first dimension).
          mat is added to the final result.
          res=(beta∗M)+(alpha∗sum(batch1i@batch2i, i=0, b))
-        * batch1 and batch2 must be 3D Tensors each containing the same number of matrices."""
+        * batch1 and batch2 must be 3D Tensors each containing the same
+        number of matrices."""
         return syft.addbmm(self, tensor2, mat, beta, alpha)
 
-
-    def  addbmm_(self, tensor2, mat, beta=1, alpha=1):
-        """Performs a inplace batch matrix-matrix product of matrices stored in batch1(tensor1) and batch2(tensor2),
-         with a reduced add step (all matrix multiplications get accumulated along the first dimension).
+    def addbmm_(self, tensor2, mat, beta=1, alpha=1):
+        """Performs a inplace batch matrix-matrix product of matrices stored
+        in batch1(tensor1) and batch2(tensor2), with a reduced add step
+        (all matrix multiplications get accumulated along the first dimension).
          mat is added to the final result.
          res=(beta∗M)+(alpha∗sum(batch1i@batch2i, i=0, b)
-        * batch1 and batch2 must be 3D Tensors each containing the same number of matrices.)"""
+        * batch1 and batch2 must be 3D Tensors each containing the same number
+        of matrices.)"""
         _ensure_tensorbase(tensor2)
         _ensure_tensorbase(mat)
         if tensor2.data.ndim != 3:
@@ -308,19 +367,23 @@ class TensorBase(object):
             sum = 0
             for i in range(len(self.data)):
                 sum += self.data[i]
-            self.data = (mat.data*beta)+(alpha*sum)
+            self.data = (mat.data * beta) + (alpha * sum)
             return self
 
     def baddbmm(self, tensor2, mat, beta=1, alpha=1):
-        """Performs a batch matrix-matrix product of matrices in batch1(tensor1) and batch2(tensor2). mat is added to the final result.
+        """Performs a batch matrix-matrix product of matrices in
+        batch1(tensor1) and batch2(tensor2). mat is added to the final result.
           resi=(beta∗Mi)+(alpha∗batch1i×batch2i)
-          *batch1 and batch2 must be 3D Tensors each containing the same number of matrices."""
+          *batch1 and batch2 must be 3D Tensors each containing the same number
+          of matrices."""
         return syft.baddbmm(self, tensor2, mat, beta, alpha)
 
     def baddbmm_(self, tensor2, mat, beta=1, alpha=1):
-        """Performs a batch matrix-matrix product of matrices in batch1(tensor1) and batch2(tensor2). mat is added to the final result.
+        """Performs a batch matrix-matrix product of matrices in
+        batch1(tensor1) and batch2(tensor2). mat is added to the final result.
           resi=(beta∗Mi)+(alpha∗batch1i×batch2i)
-          *batch1 and batch2 must be 3D Tensors each containing the same number of matrices."""
+          *batch1 and batch2 must be 3D Tensors each containing the same number
+          of matrices."""
         _ensure_tensorbase(tensor2)
         _ensure_tensorbase(mat)
         if tensor2.data.ndim != 3:
@@ -332,7 +395,7 @@ class TensorBase(object):
         else:
             self.data = np.matmul(self.data, tensor2.data)
             self.data *= alpha
-            self.data += (mat.data*beta)
+            self.data += (mat.data * beta)
             return self
 
     def __str__(self):


Test patch:
diff --git a/syft/test/benchmark.py b/syft/test/benchmark.py
index 77091539bb..743525dd68 100644
--- a/syft/test/benchmark.py
+++ b/syft/test/benchmark.py
@@ -1,10 +1,12 @@
 from time import time
 from line_profiler import LineProfiler
 
+
 class Benchmark():
     """
     This is a testing class for the benchmarking of functions.
-    Input of the Benchmark class should be the function to test and the params (optional)
+    Input of the Benchmark class should be the function to test
+    and the params (optional)
     """
 
     def __init__(self, function, **params):
@@ -21,7 +23,6 @@ class Benchmark():
             self.function(**self.params)
             t1 = time()
             results.append(t1 - t0)
-        
         return results
 
     def profile_lines(self):
@@ -33,4 +34,3 @@ class Benchmark():
         lp_wrapper = lp(self.function)
         lp_wrapper(**self.params)
         lp.print_stats()
-    
\ No newline at end of file
diff --git a/tests/test_math.py b/tests/test_math.py
index 2d966b3e49..8b21446ef7 100644
--- a/tests/test_math.py
+++ b/tests/test_math.py
@@ -21,8 +21,8 @@ class DotTests(unittest.TestCase):
 class CeilTests(unittest.TestCase):
     def testCeil(self):
             t1 = TensorBase(np.array([[2.3, 4.1], [7.4, 8.3]]))
-            self.assertTrue(syft.equal(syft.ceil(t1), TensorBase([[3.,  5.],
-                                                                 [8.,  9.]])))
+            self.assertTrue(syft.equal(syft.ceil(t1), TensorBase([[3., 5.],
+                                                                 [8., 9.]])))
 
 
 class CumsumTests(unittest.TestCase):
@@ -136,7 +136,7 @@ class addbmmTests(unittest.TestCase):
         t2 = TensorBase(np.array([[[3, 5], [5, 7]], [[7, 9], [1, 3]]]))
         mat = TensorBase(np.array([[2, 3], [3, 4]]))
         out = syft.addbmm(t1, t2, mat, beta=2, alpha=2)
-        self.assertTrue(np.array_equal(out.data, [[176,  266], [114,  172]]))
+        self.assertTrue(np.array_equal(out.data, [[176, 266], [114, 172]]))
 
 
 class baddbmmTests(unittest.TestCase):
@@ -145,5 +145,5 @@ class baddbmmTests(unittest.TestCase):
         t2 = TensorBase(np.array([[[3, 5], [5, 7]], [[7, 9], [1, 3]]]))
         mat = TensorBase(np.array([[[2, 3], [3, 4]], [[4, 5], [5, 6]]]))
         out = syft.baddbmm(t1, t2, mat, beta=2, alpha=2)
-        self.assertTrue(np.array_equal(out.data, [[[62,   92], [96,  142]],
-                                                  [[122,  184], [28,   42]]]))
+        self.assertTrue(np.array_equal(out.data, [[[62, 92], [96, 142]],
+                                                  [[122, 184], [28, 42]]]))
diff --git a/tests/test_paillier.py b/tests/test_paillier.py
new file mode 100644
index 0000000000..8f52aca3df
--- /dev/null
+++ b/tests/test_paillier.py
@@ -0,0 +1,124 @@
+from syft.he.paillier import KeyPair, PaillierTensor
+from syft import TensorBase
+import unittest
+import numpy as np
+
+
+# Here's our "unit tests".
+class DimTests(unittest.TestCase):
+    def testDimOne(self):
+        p, s = KeyPair().generate()
+        x = PaillierTensor(p, np.array([1, 2, 3, 4, 5.]))
+        self.assertTrue(x.dim() == 1)
+
+
+class AddTests(unittest.TestCase):
+
+    def testSimple(self):
+        p, s = KeyPair().generate()
+
+        x = PaillierTensor(p, np.array([1, 2, 3, 4, 5.]))
+        x2 = PaillierTensor(p, np.array([3, 4, 5, 6, 7.]))
+
+        y = (x + x2).decrypt(s)
+        self.assertTrue(y == np.array([4., 6., 8., 10., 12.]))
+
+    def testSimpleReversed(self):
+        p, s = KeyPair().generate()
+
+        x = PaillierTensor(p, np.array([1, 2, 3, 4, 5.]))
+        x2 = PaillierTensor(p, np.array([3, 4, 5, 6, 7.]))
+
+        y = (x2 + x).decrypt(s)
+        self.assertTrue(y == np.array([4., 6., 8., 10., 12.]))
+
+    def testScalarInplace(self):
+        p, s = KeyPair().generate()
+
+        x = PaillierTensor(p, np.array([1, 2, 3, 4, 5.]))
+
+        x += 1
+        self.assertTrue(s.decrypt(x) == np.array([2., 3., 4., 5., 6.]))
+
+    def testInplace(self):
+        p, s = KeyPair().generate()
+
+        x = PaillierTensor(p, np.array([1, 2, 3, 4, 5.]))
+        x2 = PaillierTensor(p, np.array([3, 4, 5, 6, 7.]))
+
+        x += x2
+        self.assertTrue(s.decrypt(x) == np.array([4., 6., 8., 10., 12.]))
+
+    def testInplaceReversed(self):
+        p, s = KeyPair().generate()
+
+        x = PaillierTensor(p, np.array([1, 2, 3, 4, 5.]))
+        x2 = PaillierTensor(p, np.array([3, 4, 5, 6, 7.]))
+
+        x2 += x
+        self.assertTrue(s.decrypt(x2) == np.array([4., 6., 8., 10., 12.]))
+
+    def testScalar(self):
+        p, s = KeyPair().generate()
+
+        x = PaillierTensor(p, np.array([1, 2, 3, 4, 5.]))
+
+        y = x + 40
+
+        self.assertTrue(s.decrypt(y) == np.array([41., 42., 43., 44., 45.]))
+
+    def testInplacePlaintext(self):
+        p, s = KeyPair().generate()
+
+        x = PaillierTensor(p, np.array([1, 2, 3, 4, 5.]))
+        x2 = TensorBase(np.array([3, 4, 5, 6, 7.]))
+
+        x += x2
+        self.assertTrue(s.decrypt(x) == np.array([4., 6., 8., 10., 12.]))
+
+
+class MulTests(unittest.TestCase):
+
+    def testBasic(self):
+        p, s = KeyPair().generate()
+
+        x = PaillierTensor(p, np.array([1, 2, 3, 4, 5.]))
+        x2 = TensorBase(np.array([3, 4, 5, 6, 7.]))
+
+        y = x * x2
+        self.assertTrue(y.decrypt(s) == np.array([3., 8., 15., 24., 35.]))
+
+    def testBasicReversed(self):
+        p, s = KeyPair().generate()
+
+        x = PaillierTensor(p, np.array([1, 2, 3, 4, 5.]))
+        x2 = TensorBase(np.array([3, 4, 5, 6, 7.]))
+
+        y = x2 * x
+        self.assertTrue(y.decrypt(s) == np.array([3., 8., 15., 24., 35.]))
+
+    def testInline(self):
+        p, s = KeyPair().generate()
+
+        x = PaillierTensor(p, np.array([1, 2, 3, 4, 5.]))
+        x2 = TensorBase(np.array([3, 4, 5, 6, 7.]))
+
+        x *= x2
+        self.assertTrue(x.decrypt(s) == np.array([3., 8., 15., 24., 35.]))
+
+    def testInlineReversed(self):
+        p, s = KeyPair().generate()
+
+        x = PaillierTensor(p, np.array([1, 2, 3, 4, 5.]))
+        x2 = TensorBase(np.array([3, 4, 5, 6, 7.]))
+
+        x2 *= x
+        self.assertTrue(x2.decrypt(s) == np.array([3., 8., 15., 24., 35.]))
+
+    def testScalar(self):
+        p, s = KeyPair().generate()
+
+        x = PaillierTensor(p, np.array([1, 2, 3, 4, 5.]))
+
+        x *= 2
+        self.assertTrue(s.decrypt(x) == np.array([2., 4., 6., 8., 10.]))
diff --git a/tests/test_tensor.py b/tests/test_tensor.py
index ac9510f627..ffbdc2eea4 100644
--- a/tests/test_tensor.py
+++ b/tests/test_tensor.py
@@ -244,14 +244,14 @@ class addbmmTests(unittest.TestCase):
         t2 = TensorBase(np.array([[[3, 5], [5, 7]], [[7, 9], [1, 3]]]))
         mat = TensorBase(np.array([[2, 3], [3, 4]]))
         out = t1.addbmm(t2, mat, beta=2, alpha=2)
-        self.assertTrue(np.array_equal(out.data, [[176,  266], [114,  172]]))
+        self.assertTrue(np.array_equal(out.data, [[176, 266], [114, 172]]))
 
     def testaddbmm_(self):
         t1 = TensorBase(np.array([[[3, 4], [5, 6]], [[7, 8], [1, 2]]]))
         t2 = TensorBase(np.array([[[3, 5], [5, 7]], [[7, 9], [1, 3]]]))
         mat = TensorBase(np.array([[2, 3], [3, 4]]))
         t1.addbmm_(t2, mat, beta=2, alpha=2)
-        self.assertTrue(np.array_equal(t1.data, [[176,  266], [114,  172]]))
+        self.assertTrue(np.array_equal(t1.data, [[176, 266], [114, 172]]))
 
 
 class baddbmmTests(unittest.TestCase):
@@ -260,13 +260,13 @@ class baddbmmTests(unittest.TestCase):
         t2 = TensorBase(np.array([[[3, 5], [5, 7]], [[7, 9], [1, 3]]]))
         mat = TensorBase(np.array([[[2, 3], [3, 4]], [[4, 5], [5, 6]]]))
         out = t1.baddbmm(t2, mat, beta=2, alpha=2)
-        self.assertTrue(np.array_equal(out.data, [[[62,   92], [96,  142]],
-                                                  [[122,  184], [28,   42]]]))
+        self.assertTrue(np.array_equal(out.data, [[[62, 92], [96, 142]],
+                                                  [[122, 184], [28, 42]]]))
 
     def testbaddbmm_(self):
         t1 = TensorBase(np.array([[[3, 4], [5, 6]], [[7, 8], [1, 2]]]))
         t2 = TensorBase(np.array([[[3, 5], [5, 7]], [[7, 9], [1, 3]]]))
         mat = TensorBase(np.array([[[2, 3], [3, 4]], [[4, 5], [5, 6]]]))
         t1.baddbmm_(t2, mat, beta=2, alpha=2)
-        self.assertTrue(np.array_equal(t1.data, [[[62,   92], [96,  142]],
-                                                 [[122,  184], [28,   42]]]))
+        self.assertTrue(np.array_equal(t1.data, [[[62, 92], [96, 142]],
+                                                 [[122, 184], [28, 42]]]))