diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,9 @@
+# Changelog
+
+## 0.2.0 (2017-06-08)
+
+* `Group` typeclass split into `Group` and `AbelianGroup` typeclasses
+
+## 0.1.0 (2017-05-28)
+
+Initial release
diff --git a/cmd/interop-entrypoint/Main.hs b/cmd/interop-entrypoint/Main.hs
--- a/cmd/interop-entrypoint/Main.hs
+++ b/cmd/interop-entrypoint/Main.hs
@@ -18,7 +18,7 @@
 import Protolude hiding (group)
 
 import Crypto.Hash (SHA256(..))
-import qualified Data.ByteString.Base16 as Base16
+import Data.ByteArray.Encoding (convertFromBase, convertToBase, Base(Base16))
 import Options.Applicative
 import System.IO (hFlush, hGetLine, hPutStrLn)
 
@@ -38,7 +38,7 @@
   , elementToMessage
   , formatError
   )
-import Crypto.Spake2.Group (Group(..))
+import Crypto.Spake2.Group (AbelianGroup, Group(..))
 import Crypto.Spake2.Groups (Ed25519(..))
 
 
@@ -69,7 +69,7 @@
 
 
 runInteropTest
-  :: (HasCallStack, Group group)
+  :: (HasCallStack, AbelianGroup group)
   => Protocol group SHA256
   -> Password
   -> Handle
@@ -80,9 +80,9 @@
   let outElement = computeOutboundMessage spake2
   output (elementToMessage protocol outElement)
   line <- hGetLine inH
-  let inMsg = parseHex (toS line)
+  let inMsg = parseHex (toS line :: ByteString)
   case inMsg of
-    Left err -> abort err
+    Left err -> abort (toS err)
     Right inMsgBytes ->
       case extractElement protocol inMsgBytes of
         Left err -> abort $ "Could not handle incoming message (line = " <> show line <> ", msgBytes = " <>  show inMsgBytes <> "): " <> formatError err
@@ -94,13 +94,13 @@
 
   where
     output message = do
-      hPutStrLn outH (toS (Base16.encode message))
+      hPutStrLn outH (toS (convertToBase Base16 message :: ByteString))
       hFlush outH
 
     parseHex line =
-      case Base16.decode line of
-        (bytes, "") -> Right bytes
-        _ -> Left ("Could not decode line: " <> show line)
+      case convertFromBase Base16 line of
+        Left err -> Left ("Could not decode line (reason: " <> err <> "): " <> show line)
+        Right bytes -> Right bytes
 
 
 makeProtocolFromSide :: Side -> Protocol Ed25519 SHA256
diff --git a/spake2.cabal b/spake2.cabal
--- a/spake2.cabal
+++ b/spake2.cabal
@@ -3,7 +3,7 @@
 -- see: https://github.com/sol/hpack
 
 name:           spake2
-version:        0.1.0
+version:        0.2.0
 synopsis:       Implementation of the SPAKE2 Password-Authenticated Key Exchange algorithm
 description:    This library implements the SPAKE2 password-authenticated key exchange
                 ("PAKE") algorithm. This allows two parties, who share a weak password, to
@@ -18,6 +18,9 @@
 build-type:     Simple
 cabal-version:  >= 1.10
 
+extra-source-files:
+    CHANGELOG.md
+
 source-repository head
   type: git
   location: https://github.com/jml/haskell-spake2
@@ -38,7 +41,6 @@
       Crypto.Spake2.Group
       Crypto.Spake2.Groups
       Crypto.Spake2.Groups.Ed25519
-      Crypto.Spake2.Groups.IntegerAddition
       Crypto.Spake2.Groups.IntegerGroup
       Crypto.Spake2.Math
       Crypto.Spake2.Util
@@ -53,8 +55,8 @@
   build-depends:
       base >= 4.9 && < 5
     , protolude
-    , base16-bytestring
     , cryptonite
+    , memory
     , optparse-applicative
     , spake2
   default-language: Haskell2010
diff --git a/src/Crypto/Spake2.hs b/src/Crypto/Spake2.hs
--- a/src/Crypto/Spake2.hs
+++ b/src/Crypto/Spake2.hs
@@ -84,8 +84,7 @@
 -}
 
 module Crypto.Spake2
-  ( something
-  , Password
+  ( Password
   , makePassword
   -- * The SPAKE2 protocol
   , Protocol
@@ -108,20 +107,15 @@
 import Crypto.Error (CryptoError, CryptoFailable(..))
 import Crypto.Hash (HashAlgorithm, hashWith)
 import Crypto.Random.Types (MonadRandom(..))
-import Data.ByteArray (ByteArrayAccess, ByteArray)
+import Data.ByteArray (ByteArrayAccess)
 import qualified Data.ByteArray as ByteArray
 import qualified Data.ByteString as ByteString
 
-import Crypto.Spake2.Group (Group(..), decodeScalar, scalarSizeBytes)
+import Crypto.Spake2.Group (AbelianGroup(..), Group(..), decodeScalar, scalarSizeBytes)
 import qualified Crypto.Spake2.Math as Math
 import Crypto.Spake2.Util (expandData)
 
 
--- | Do-nothing function so that we have something to import in our tests.
--- TODO: Actually test something genuine and then remove this.
-something :: a -> a
-something x = x
-
 -- | Shared secret password used to negotiate the connection.
 --
 -- Constructor deliberately not exported,
@@ -138,17 +132,12 @@
 newtype SideID = SideID { unSideID :: ByteString } deriving (Eq, Ord, Show)
 
 -- | Convert a user-supplied password into a scalar on a group.
-passwordToScalar :: Group group => group -> Password -> Scalar group
+passwordToScalar :: AbelianGroup group => group -> Password -> Scalar group
 passwordToScalar group password =
-  let oversized = expandPassword password (scalarSizeBytes group + 16) :: ByteString
-  in decodeScalar group oversized
-
--- | Expand a password using HKDF so that it has a certain number of bytes.
---
--- TODO: jml cannot remember why you might want to call this.
-expandPassword :: ByteArray output => Password -> Int -> output
-expandPassword (Password bytes) numBytes = expandData info bytes numBytes
+  decodeScalar group oversized
   where
+    oversized = expandPassword password (scalarSizeBytes group + 16) :: ByteString
+    expandPassword (Password bytes) = expandData info bytes
     -- This needs to be exactly "SPAKE2 pw"
     -- See <https://github.com/bitwiseshiftleft/sjcl/pull/273/#issuecomment-185251593>
     info = "SPAKE2 pw"
@@ -215,16 +204,14 @@
 
 -- | Relation between two sides in SPAKE2.
 -- Can be either symmetric (both sides are the same), or asymmetric.
---
--- XXX: Maybe too generic? Could reasonably replace 'a' with 'Side group'.
-data Relation a
+data Relation group
   = Asymmetric
-  { sideA :: a -- ^ Side A. Both sides need to agree who side A is.
-  , sideB :: a -- ^ Side B. Both sides need to agree who side B is.
+  { sideA :: Side group -- ^ Side A. Both sides need to agree who side A is.
+  , sideB :: Side group -- ^ Side B. Both sides need to agree who side B is.
   , us :: WhichSide -- ^ Which side we are
   }
   | Symmetric
-  { bothSides :: a -- ^ Description used by both sides.
+  { bothSides :: Side group -- ^ Description used by both sides.
   }
 
 theirPrefix :: Relation a -> Word8
@@ -245,7 +232,7 @@
   = Protocol
   { group :: group -- ^ The group to use for encryption
   , hashAlgorithm :: hashAlgorithm -- ^ Hash algorithm used for generating the session key
-  , relation :: Relation (Side group)  -- ^ How the two sides relate to each other
+  , relation :: Relation group  -- ^ How the two sides relate to each other
   }
 
 -- | Construct an asymmetric SPAKE2 protocol.
@@ -290,7 +277,7 @@
 
 -- | Commence a SPAKE2 exchange.
 startSpake2
-  :: (MonadRandom randomly, Group group)
+  :: (MonadRandom randomly, AbelianGroup group)
   => Protocol group hashAlgorithm
   -> Password
   -> randomly (Math.Spake2Exchange group)
diff --git a/src/Crypto/Spake2/Group.hs b/src/Crypto/Spake2/Group.hs
--- a/src/Crypto/Spake2/Group.hs
+++ b/src/Crypto/Spake2/Group.hs
@@ -1,10 +1,11 @@
 {-# LANGUAGE TypeFamilies #-}
 {-|
 Module: Crypto.Spake2.Group
-Description: Interface for mathematical groups
+Description: Interfaces for mathematical groups
 -}
 module Crypto.Spake2.Group
-  ( Group(..)
+  ( AbelianGroup(..)
+  , Group(..)
   , decodeScalar
   , elementSizeBytes
   , scalarSizeBytes
@@ -19,24 +20,12 @@
 
 import Crypto.Spake2.Util (bytesToNumber)
 
+
 -- | A mathematical group intended to be used with SPAKE2.
---
--- Notes:
---
---  * This is a much richer interface than one would expect from a group purely derived from abstract algebra
---  * jml thinks this is relevant to all Diffie-Hellman cryptography,
---    but too ignorant to say for sure
---  * Is this group automatically abelian? cyclic?
---    Must it have these properties?
 class Group group where
   -- | An element of the group.
   type Element group :: *
 
-  -- | A scalar for this group.
-  -- Mathematically equivalent to an integer,
-  -- but possibly stored differently for computational reasons.
-  type Scalar group :: *
-
   -- | Group addition.
   --
   -- prop> \x y z -> elementAdd group (elementAdd group x y) z == elementAdd group x (elementAdd group y z)
@@ -62,10 +51,86 @@
   -- prop> \x -> (elementAdd group groupIdentity x) == x
   groupIdentity :: group -> Element group
 
+  -- | Encode an element of the group into bytes.
+  --
+  -- Note [Byte encoding in Group]
+  --
+  -- prop> \x -> decodeElement group (encodeElement group x) == CryptoPassed x
+  encodeElement :: ByteArray bytes => group -> Element group -> bytes
+
+  -- | Decode an element into the group from some bytes.
+  --
+  -- Note [Byte encoding in Group]
+  decodeElement :: ByteArray bytes => group -> bytes -> CryptoFailable (Element group)
+
+  -- | Size of elements, in bits
+  elementSizeBits :: group -> Int
+
+  -- | Deterministically create an arbitrary element from a seed bytestring.
+  --
+  -- __XXX__: jml would much rather this take a scalar, an element, or even an integer, rather than bytes
+  -- because bytes mean that the group instances have to know about hash algorithms and HKDF.
+  -- If the IntegerGroup class in SPAKE2 also oversized its input,
+  -- then it and the ed25519 implementation would have identical decoding.
+  arbitraryElement :: ByteArrayAccess bytes => group -> bytes -> Element group
+
+
+-- | A group where 'elementAdd' is commutative.
+--
+-- That is, where
+--
+-- prop> \x y -> elementAdd group x y == elementAdd group y x
+--
+-- This property leads to a natural \(\mathbb{Z}\)-module,
+-- where scalar multiplication is defined as repeatedly calling `elementAdd`.
+--
+-- === Definitions
+--
+-- Warning: this gets algebraic.
+--
+-- A /module/ is a ring \(R\) together with an abelian group \((G, +)\),
+-- and a new operator \(\cdot\) (i.e. scalar multiplication)
+-- such that:
+--
+-- 1. \(r \cdot (x + y) = r \cdot x + r \cdot y\)
+-- 2. \((r + s) \cdot x = r \cdot x + s \cdot x\)
+-- 3. \((rs) \cdot x = r \cdot (s \cdot x)\)
+-- 4. \(1_R \cdot x = x\)
+--
+-- for all \(x, y\) in \(G\), and \(r, s\) in \(R\),
+-- where \(1_R\) is the identity of the ring.
+--
+-- A /ring/ \(R, +, \cdot\) a set \(R\) with two operators such that:
+--
+-- 1. \(R\) is an abelian group under \(+\)
+-- 2. \(R\) is a monoid under \(\cdot\)
+-- 3. \(cdot\) is _distributive_ with respect to \(+\). That is,
+--    1. \(a \cdot (b + c) = (a \cdot b) + (a \cdot c) (left distributivity)
+--    2. \((b + c) \cdot a) = (b \cdot a) + (c \cdot a) (right distributivity)
+--
+-- Note we have to define left & right distributivity,
+-- because \(\cdot\) might not be commutative.
+--
+-- A /monoid/ is a group without the notion of inverse. See Haskell's 'Monoid' typeclass.
+--
+-- A \(\mathbb{Z}\)-module is a module where the ring \(R\)
+-- is the integers with normal addition and multiplication.
+class Group group => AbelianGroup group where
+  -- | A scalar for this group.
+  -- Mathematically equivalent to an integer,
+  -- but possibly stored differently for computational reasons.
+  type Scalar group :: *
+
   -- | Multiply an element of the group with respect to a scalar.
   --
   -- This is equivalent to adding the element to itself N times, where N is a scalar.
+  -- The default implementation does exactly that.
   scalarMultiply :: group -> Scalar group -> Element group -> Element group
+  scalarMultiply group scalar element =
+    scalarMultiply' (scalarToInteger group scalar) element
+    where
+      scalarMultiply' 0 _ = groupIdentity group
+      scalarMultiply' n x = elementAdd group x (scalarMultiply' (n - 1) x)
 
   -- | Get the scalar that corresponds to an integer.
   --
@@ -81,39 +146,16 @@
   -- prop> \x -> integerToScalar group (scalarToInteger group x) == x
   scalarToInteger :: group -> Scalar group -> Integer
 
-  -- | Encode an element of the group into bytes.
-  --
-  -- Note [Byte encoding in Group]
-  --
-  -- prop> \x -> decodeElement group (encodeElement group x) == CryptoPassed x
-  encodeElement :: ByteArray bytes => group -> Element group -> bytes
-
-  -- | Decode an element into the group from some bytes.
-  --
-  -- Note [Byte encoding in Group]
-  decodeElement :: ByteArray bytes => group -> bytes -> CryptoFailable (Element group)
+  -- | Size of scalars, in bits
+  scalarSizeBits :: group -> Int
 
   -- | Encode a scalar into bytes.
   -- | Generate a new random element of the group, with corresponding scalar.
   generateElement :: MonadRandom randomly => group -> randomly (KeyPair group)
 
-  -- | Size of elements, in bits
-  elementSizeBits :: group -> Int
 
-  -- | Size of scalars, in bits
-  scalarSizeBits :: group -> Int
-
-  -- | Deterministically create an arbitrary element from a seed bytestring.
-  --
-  -- __XXX__: jml would much rather this take a scalar, an element, or even an integer, rather than bytes
-  -- because bytes mean that the group instances have to know about hash algorithms and HKDF.
-  -- If the IntegerGroup class in SPAKE2 also oversized its input,
-  -- then it and the ed25519 implementation would have identical decoding.
-  arbitraryElement :: ByteArrayAccess bytes => group -> bytes -> Element group
-
-
 -- | Map some arbitrary bytes into a scalar in a group.
-decodeScalar :: (ByteArrayAccess bytes, Group group) => group -> bytes -> Scalar group
+decodeScalar :: (ByteArrayAccess bytes, AbelianGroup group) => group -> bytes -> Scalar group
 decodeScalar group bytes = integerToScalar group (bytesToNumber bytes)
 
 -- | Size of elements in a group, in bits.
@@ -121,7 +163,7 @@
 elementSizeBytes group = (elementSizeBits group + 7) `div` 8
 
 -- | Size of scalars in a group, in bytes.
-scalarSizeBytes :: Group group => group -> Int
+scalarSizeBytes :: AbelianGroup group => group -> Int
 scalarSizeBytes group = (scalarSizeBits group + 7) `div` 8
 
 -- | A group key pair composed of the private part (a scalar)
@@ -133,6 +175,19 @@
   }
 
 {-
+Note [Algebra]
+~~~~~~~~~~~~~~
+
+* Perhaps we should call 'AbelianGroup' 'ZModule' or similar?
+* A "proper" implementation would no doubt have a Ring typeclass
+  and then a new Module typeclass that somehow composed a Ring and an AbelianGroup.
+  This seems unnecessary for our implementation needs,
+  and is perhaps best left to those who know something about designing algebraic libraries.
+* Cyclic groups are necessarily abelian.
+
+-}
+
+{-
 Note [Byte encoding in Group]
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -142,15 +197,19 @@
 
  * cryptonite does it with 'EllipticCurve'
  * warner does it with spake2.groups
+ * you just need to send different stuff over the wire for elliptic curve groups
+   than integer modulo groups
 
 Reasons against:
 
  * mathematical structure of groups has no connection to serialization
  * might want multiple encodings for same mathematical group
+   (this seems unlikely)
 
-Including for now on the assumption that I'm ignorant.
+We're keeping encode/decode in for now.
+Later, we might want to split it out into a different typeclass,
+perhaps one that inherits from the base 'Group' class.
 
-TODO: Revisit decision to put byte encoding in Group after we've done a couple of implementations
 -}
 
 {-
diff --git a/src/Crypto/Spake2/Groups.hs b/src/Crypto/Spake2/Groups.hs
--- a/src/Crypto/Spake2/Groups.hs
+++ b/src/Crypto/Spake2/Groups.hs
@@ -9,10 +9,7 @@
   , IntegerGroup.IntegerGroup(..)
   , IntegerGroup.makeIntegerGroup
   , IntegerGroup.i1024
-  -- * For testing only
-  , IntegerAddition.IntegerAddition(..)
   ) where
 
 import qualified Crypto.Spake2.Groups.Ed25519 as Ed25519
-import qualified Crypto.Spake2.Groups.IntegerAddition as IntegerAddition
 import qualified Crypto.Spake2.Groups.IntegerGroup as IntegerGroup
diff --git a/src/Crypto/Spake2/Groups/Ed25519.hs b/src/Crypto/Spake2/Groups/Ed25519.hs
--- a/src/Crypto/Spake2/Groups/Ed25519.hs
+++ b/src/Crypto/Spake2/Groups/Ed25519.hs
@@ -28,7 +28,7 @@
 import qualified Data.ByteArray as ByteArray
 import qualified Data.List as List
 
-import Crypto.Spake2.Group (Group(..), KeyPair(..), scalarSizeBytes)
+import Crypto.Spake2.Group (AbelianGroup(..), Group(..), KeyPair(..), scalarSizeBytes)
 import Crypto.Spake2.Util (bytesToNumber, expandArbitraryElementSeed)
 
 {-
@@ -62,30 +62,18 @@
 data Ed25519 = Ed25519 deriving (Eq, Show)
 
 instance Group Ed25519 where
-  type Scalar Ed25519 = Integer
   type Element Ed25519 = ExtendedPoint 'Member
 
   elementAdd _ x y = addExtendedPoints x y
   elementNegate group = scalarMultiply group (l - 1)
   groupIdentity _ = assertInGroup extendedZero
-  scalarMultiply _ n x = safeScalarMultiply n x
 
-  integerToScalar _ x = x
-  scalarToInteger _ x = x
-
   encodeElement _ x = encodeAffinePoint (extendedToAffine' x)
   decodeElement _ bytes = toCryptoFailable $ do
-    affine <- decodeAffinePoint bytes
-    let extended = affineToExtended affine
+    extended <- affineToExtended <$> decodeAffinePoint bytes
     ensureInGroup extended
 
-  generateElement group = do
-    scalar <- generateMax l
-    let element = scalarMultiply group scalar generator
-    pure (KeyPair element scalar)
-
   elementSizeBits _ = 255
-  scalarSizeBits _ = 255
 
   arbitraryElement group bytes =
     let seed = expandArbitraryElementSeed bytes (scalarSizeBytes group + 16) :: ByteString
@@ -93,6 +81,22 @@
     in
     List.head [ element | Right element <- map makeGroupMember [y..] ]
 
+instance AbelianGroup Ed25519 where
+  type Scalar Ed25519 = Integer
+
+  scalarMultiply _ n x = safeScalarMultiply n x
+
+  integerToScalar _ x = x
+  scalarToInteger _ x = x
+
+  scalarSizeBits _ = 255
+
+  generateElement group = do
+    scalar <- generateMax l
+    let element = scalarMultiply group scalar generator
+    pure (KeyPair element scalar)
+
+
 -- | Errors that can occur within the group.
 data Error
   = NotOnCurve Integer Integer
@@ -302,7 +306,6 @@
 -- | Attempt to create a member of Ed25519 from an affine @y@ coordinate.
 makeGroupMember :: Integer -> Either Error (Element Ed25519)
 makeGroupMember y = do
-  -- XXX: Similar to decodeElement. Can we share code?
   point <- affineToExtended <$> makeAffinePoint (xRecover y) y
   let point8 = safeScalarMultiply 8 point
   if isExtendedZero point8
@@ -337,7 +340,11 @@
 which would crash the program if incorrect.
 For programming convenience, I just skip these values.
 
-jml doesn't know what is meant by the 'order' of a point.
+The 'order' of a point \(x\) is the number \(n\) such that:
+'scalarMultiply group (integerToScalar group n) x == groupIdentity group'
+
+Note this is different from the order of a /group/,
+which for finite groups is the number of elements in the group.
 
 -}
 
diff --git a/src/Crypto/Spake2/Groups/IntegerAddition.hs b/src/Crypto/Spake2/Groups/IntegerAddition.hs
deleted file mode 100644
--- a/src/Crypto/Spake2/Groups/IntegerAddition.hs
+++ /dev/null
@@ -1,64 +0,0 @@
-{-# OPTIONS_HADDOCK hide #-}
-{-# LANGUAGE TypeFamilies #-}
-{-|
-Module: Crypto.Spake2.Groups
-Description: Additive group of integers modulo \(n\)
-
-Do __NOT__ use this for anything cryptographic.
--}
-module Crypto.Spake2.Groups.IntegerAddition
-  ( IntegerAddition(..)
-  ) where
-
-import Protolude hiding (group, length)
-
-import Crypto.Error (CryptoFailable(..))
-import Crypto.Number.Basic (numBits)
-import Crypto.Number.ModArithmetic (expSafe)
-import Crypto.Random.Types (MonadRandom(..))
-
-import Crypto.Spake2.Group
-  ( Group(..)
-  , KeyPair(..)
-  , decodeScalar
-  , elementSizeBytes
-  , scalarSizeBytes
-  )
-import Crypto.Spake2.Util
-  ( expandArbitraryElementSeed
-  , bytesToNumber
-  , unsafeNumberToBytes
-  )
-
--- | Simple integer addition group.
---
--- Do __NOT__ use this for anything cryptographic.
-newtype IntegerAddition = IntegerAddition { modulus :: Integer } deriving (Eq, Ord, Show)
-
-instance Group IntegerAddition where
-  type Element IntegerAddition = Integer
-  type Scalar IntegerAddition = Integer
-
-  elementAdd group x y = (x + y) `mod` modulus group
-  elementNegate group x = negate x `mod` modulus group
-  elementSubtract group x y = (x - y) `mod` modulus group
-  groupIdentity _ = 0
-  scalarMultiply group n x = (n * x) `mod` modulus group
-  integerToScalar _ x = x
-  scalarToInteger _ x = x
-  encodeElement group x = unsafeNumberToBytes (elementSizeBytes group) (x `mod` modulus group)
-  decodeElement _ bytes = CryptoPassed (bytesToNumber bytes)
-  generateElement group = do
-    scalarBytes <- getRandomBytes (scalarSizeBytes group)
-    let scalar = decodeScalar group (scalarBytes :: ByteString)
-    let element = scalarMultiply group scalar (groupIdentity group)
-    pure (KeyPair element scalar)
-  scalarSizeBits group = numBits (modulus group)  -- XXX: Incorrect value. Not sure what it should be.
-  elementSizeBits group = numBits (modulus group) -- XXX: should be size of subgroup
-  arbitraryElement group seed =
-    let processedSeed = expandArbitraryElementSeed seed (elementSizeBytes group) :: ByteString
-        r = (modulus group - 1) `div` modulus group -- XXX: should be size of subgroup
-        h = bytesToNumber processedSeed `mod` modulus group
-    in expSafe h r (modulus group)
-
-
diff --git a/src/Crypto/Spake2/Groups/IntegerGroup.hs b/src/Crypto/Spake2/Groups/IntegerGroup.hs
--- a/src/Crypto/Spake2/Groups/IntegerGroup.hs
+++ b/src/Crypto/Spake2/Groups/IntegerGroup.hs
@@ -19,7 +19,8 @@
 import Crypto.Number.ModArithmetic (expSafe)
 
 import Crypto.Spake2.Group
-  ( Group(..)
+  ( AbelianGroup(..)
+  , Group(..)
   , KeyPair(..)
   , elementSizeBytes
   )
@@ -52,15 +53,11 @@
 
 instance Group IntegerGroup where
   type Element IntegerGroup = Integer
-  type Scalar IntegerGroup = Integer
 
   elementAdd group x y = (x * y) `mod` order group
   -- At a guess, negation is scalar multiplication where the scalar is -1
   elementNegate group x = expSafe x (subgroupOrder group - 1) (order group)
   groupIdentity _ = 1
-  scalarMultiply group n x = expSafe x (n `mod` subgroupOrder group) (order group)
-  integerToScalar group x = x `mod` subgroupOrder group  -- XXX: Should we instead fail?
-  scalarToInteger _ n = n
   encodeElement group = unsafeNumberToBytes (elementSizeBytes group)
   decodeElement group bytes =
     case bytesToNumber bytes of
@@ -68,11 +65,6 @@
         | x <= 0 || x >= order group -> CryptoFailed CryptoError_PointSizeInvalid
         | expSafe x (subgroupOrder group) (order group) /= groupIdentity group -> CryptoFailed CryptoError_PointCoordinatesInvalid
         | otherwise -> CryptoPassed x
-  generateElement group = do
-    scalar <- generateMax (subgroupOrder group)
-    let element = scalarMultiply group scalar (generator group)
-    pure (KeyPair element scalar)
-  scalarSizeBits group = numBits (subgroupOrder group)
   elementSizeBits group = numBits (order group)
   arbitraryElement group seed =
     let processedSeed = expandArbitraryElementSeed seed (elementSizeBytes group) :: ByteString
@@ -81,6 +73,21 @@
         r = (p - 1) `div` q
         h = bytesToNumber processedSeed `mod` p
     in expSafe h r p
+
+
+instance AbelianGroup IntegerGroup where
+  type Scalar IntegerGroup = Integer
+
+  scalarMultiply group n x = expSafe x (n `mod` subgroupOrder group) (order group)
+  integerToScalar group x = x `mod` subgroupOrder group
+  scalarToInteger _ n = n
+
+  generateElement group = do
+    scalar <- generateMax (subgroupOrder group)
+    let element = scalarMultiply group scalar (generator group)
+    pure (KeyPair element scalar)
+  scalarSizeBits group = numBits (subgroupOrder group)
+
 
 -- | 1024 bit integer group.
 --
diff --git a/src/Crypto/Spake2/Math.hs b/src/Crypto/Spake2/Math.hs
--- a/src/Crypto/Spake2/Math.hs
+++ b/src/Crypto/Spake2/Math.hs
@@ -91,8 +91,8 @@
 -}
 
 module Crypto.Spake2.Math
-  ( Spake2(..)  -- XXX: Not sure want to export innards but it disables "unused" warning
-  , Params(..)  -- XXX: ditto
+  ( Spake2(..)
+  , Params(..)
   , startSpake2
   , Spake2Exchange
   , computeOutboundMessage
@@ -103,7 +103,7 @@
 
 import Crypto.Random.Types (MonadRandom(..))
 
-import Crypto.Spake2.Group (Group(..), KeyPair(..))
+import Crypto.Spake2.Group (AbelianGroup(..), Group(..), KeyPair(..))
 
 -- | The parameters of the SPAKE2 protocol. The other side needs to be using
 -- the same values, but with swapped values for 'ourBlind' and 'theirBlind'.
@@ -135,11 +135,11 @@
 
 -- | Initiate the SPAKE2 exchange. Generates a secret (@xy@) that will be held
 -- by this side, and transmitted to the other side in "blinded" form.
-startSpake2 :: (Group group, MonadRandom randomly) => Spake2 group -> randomly (Spake2Exchange group)
+startSpake2 :: (AbelianGroup group, MonadRandom randomly) => Spake2 group -> randomly (Spake2Exchange group)
 startSpake2 spake2' = Started spake2' <$> generateElement (group . params $ spake2')
 
 -- | Determine the element (either \(X^{\star}\) or \(Y^{\star}\)) to send to the other side.
-computeOutboundMessage :: Group group => Spake2Exchange group -> Element group
+computeOutboundMessage :: AbelianGroup group => Spake2Exchange group -> Element group
 computeOutboundMessage Started{spake2 = Spake2{params = Params{group, ourBlind}, password}, xy} =
   elementAdd group (keyPairPublic xy) (scalarMultiply group password ourBlind)
 
@@ -161,7 +161,7 @@
 -- * \(K\) is the result of this function
 -- * \(pw\) is the password (this is what makes it SPAKE2, not SPAKE1)
 generateKeyMaterial
-  :: Group group
+  :: AbelianGroup group
   => Spake2Exchange group  -- ^ An initiated SPAKE2 exchange
   -> Element group  -- ^ The outbound message from the other side (i.e. inbound to us)
   -> Element group -- ^ The final piece of key material to generate the session key.
diff --git a/src/Crypto/Spake2/Util.hs b/src/Crypto/Spake2/Util.hs
--- a/src/Crypto/Spake2/Util.hs
+++ b/src/Crypto/Spake2/Util.hs
@@ -17,10 +17,6 @@
 import qualified Crypto.KDF.HKDF as HKDF
 import Data.ByteArray (ByteArray, ByteArrayAccess(..))
 
--- TODO: memory package (a dependency of cryptonite) has
--- Data.ByteArray.Encoding, which does base16 encoding. Perhaps we should use
--- that rather than third-party base16-bytestring library.
-
 -- | Take an arbitrary sequence of bytes and expand it to be the given number
 -- of bytes. Do this by extracting a pseudo-random key and expanding it using
 -- HKDF.
diff --git a/tests/Groups.hs b/tests/Groups.hs
--- a/tests/Groups.hs
+++ b/tests/Groups.hs
@@ -5,15 +5,13 @@
 import Protolude hiding (group)
 
 import Crypto.Error (CryptoFailable(..))
-import GHC.Base (String)
 import Test.QuickCheck (Gen, (===), arbitrary, forAll, property)
 import Test.Tasty (TestTree)
 import Test.Tasty.Hspec (Spec, testSpec, describe, it, shouldBe)
 
-import Crypto.Spake2.Group (Group(..))
+import Crypto.Spake2.Group (AbelianGroup(..), Group(..))
 import Crypto.Spake2.Groups
-  ( IntegerAddition(..)
-  , IntegerGroup(..)
+  ( IntegerGroup(..)
   , Ed25519(..)
   , i1024)
 import qualified Crypto.Spake2.Groups.Ed25519 as Ed25519
@@ -21,31 +19,29 @@
 
 tests :: IO TestTree
 tests = testSpec "Groups" $ do
-  groupProperties "integer addition modulo 7" (IntegerAddition 7) 1 (makeScalar 7)
-  groupProperties "integer group" i1024 (IntegerGroup.generator i1024) (makeScalar (subgroupOrder i1024))
-  groupProperties "Ed25519" Ed25519 Ed25519.generator (makeScalar Ed25519.l)
-
-
-makeScalar :: Integer -> Gen Integer
-makeScalar k = do
-  i <- arbitrary
-  pure $ i `mod` k
+  describe "integer group" $
+    allGroupProperties i1024 (makeScalar (subgroupOrder i1024)) (IntegerGroup.generator i1024)
+  describe "Ed25519" $
+    allGroupProperties Ed25519 (makeScalar Ed25519.l) Ed25519.generator
 
-makeElement :: Group group => group -> Gen (Scalar group) -> Element group -> Gen (Element group)
-makeElement group scalars base = do
-  scalar <- scalars
-  pure (scalarMultiply group scalar base)
+allGroupProperties
+  :: (Show (Scalar group), Show (Element group), Eq (Scalar group), Eq (Element group), AbelianGroup group)
+  => group
+  -> Gen (Scalar group)
+  -> Element group
+  -> Spec
+allGroupProperties group scalars base = do
+  describe "is a group" $ groupProperties group (makeElement group scalars base)
+  describe "is an abelian group" $ abelianGroupProperties group scalars base
 
 groupProperties
-  :: (Group group, Eq (Element group), Eq (Scalar group), Show (Element group), Show (Scalar group))
-  => String
-  -> group
-  -> Element group
-  -> Gen (Scalar group)
+  :: (Group group, Eq (Element group), Show (Element group))
+  => group
+  -> Gen (Element group)
   -> Spec
-groupProperties name group base scalars = describe name $ do
+groupProperties group elements = do
   it "addition is associative" $ property $
-    forAll triples $ \(x, y, z) -> elementAdd group (elementAdd group x y) z === elementAdd group x (elementAdd group y z)
+    forAll (triples elements) $ \(x, y, z) -> elementAdd group (elementAdd group x y) z === elementAdd group x (elementAdd group y z)
 
   it "addition with inverse yields identity" $ property $
     forAll elements $ \x -> elementAdd group x (elementNegate group x) === groupIdentity group
@@ -57,7 +53,7 @@
     elementNegate group (groupIdentity group) `shouldBe` groupIdentity group
 
   it "subtraction is negated addition" $ property $
-    forAll pairs $ \(x, y) -> elementSubtract group x y === elementAdd group x (elementNegate group y)
+    forAll (pairs elements) $ \(x, y) -> elementSubtract group x y === elementAdd group x (elementNegate group y)
 
   it "right-hand addition with identity yields original" $ property $
     forAll elements $ \x -> elementAdd group x (groupIdentity group) === x
@@ -69,6 +65,17 @@
     forAll elements $ \x -> let bytes = encodeElement group x :: ByteString
                             in decodeElement group bytes == CryptoPassed x
 
+
+abelianGroupProperties
+  :: (AbelianGroup group, Eq (Element group), Eq (Scalar group), Show (Element group), Show (Scalar group))
+  => group
+  -> Gen (Scalar group)
+  -> Element group
+  -> Spec
+abelianGroupProperties group scalars base = do
+  it "addition is commutative" $ property $
+    forAll (pairs elements) $ \(x, y) -> elementAdd group x y === elementAdd group y x
+
   it "scalar to integer roundtrips" $ property $
     forAll scalars $ \n -> integerToScalar group (scalarToInteger group n) === n
 
@@ -90,13 +97,27 @@
   where
     elements = makeElement group scalars base
 
-    pairs = do
-      x <- elements
-      y <- elements
-      pure (x, y)
+-- | Generate pairs of a thing.
+pairs :: Gen a -> Gen (a, a)
+pairs gen = do
+  x <- gen
+  y <- gen
+  pure (x, y)
 
-    triples = do
-      x <- elements
-      y <- elements
-      z <- elements
-      pure (x, y, z)
+-- | Generate triples of a thing.
+triples :: Gen a -> Gen (a, a, a)
+triples gen = do
+  x <- gen
+  y <- gen
+  z <- gen
+  pure (x, y, z)
+
+makeScalar :: Integer -> Gen Integer
+makeScalar k = do
+  i <- arbitrary
+  pure $ i `mod` k
+
+makeElement :: AbelianGroup group => group -> Gen (Scalar group) -> Element group -> Gen (Element group)
+makeElement group scalars base = do
+  scalar <- scalars
+  pure (scalarMultiply group scalar base)
diff --git a/tests/Spake2.hs b/tests/Spake2.hs
--- a/tests/Spake2.hs
+++ b/tests/Spake2.hs
@@ -1,13 +1,89 @@
 module Spake2 (tests) where
 
-import Protolude
+import Protolude hiding (group)
 import Test.Tasty (TestTree)
-import Test.Tasty.Hspec (testSpec, describe, it, shouldBe)
+import Test.Tasty.Hspec (testSpec, describe, it, shouldBe, shouldNotBe)
 
+import Crypto.Hash (SHA256(..))
 import qualified Crypto.Spake2 as Spake2
+import qualified Crypto.Spake2.Group as Group
+import Crypto.Spake2.Groups (Ed25519(..))
 
 tests :: IO TestTree
 tests = testSpec "Spake2" $ do
-  describe "something" $
-    it "should do things" $
-      Spake2.something (2 :: Int) `shouldBe` 2
+  describe "Asymmetric protocol" $ do
+    it "Produces matching session keys when passwords match" $ do
+      let password = Spake2.makePassword "abc"
+      let hashAlg = SHA256
+      let group = Ed25519
+      let m = Group.arbitraryElement group ("M" :: ByteString)
+      let n = Group.arbitraryElement group ("N" :: ByteString)
+      let idA = Spake2.SideID ""
+      let idB = Spake2.SideID ""
+      let protocolA = Spake2.makeAsymmetricProtocol hashAlg group m n idA idB Spake2.SideA
+      let protocolB = Spake2.makeAsymmetricProtocol hashAlg group m n idA idB Spake2.SideB
+      sideA <- Spake2.startSpake2 protocolA password
+      sideB <- Spake2.startSpake2 protocolB password
+      let aOut = Spake2.computeOutboundMessage sideA
+      let bOut = Spake2.computeOutboundMessage sideB
+      let aKey = Spake2.generateKeyMaterial sideA bOut
+      let bKey = Spake2.generateKeyMaterial sideB aOut
+      let aSessionKey = Spake2.createSessionKey protocolA aOut bOut aKey password
+      let bSessionKey = Spake2.createSessionKey protocolA aOut bOut bKey password
+      aSessionKey `shouldBe` bSessionKey
+    it "Produces differing session keys when passwords do not match" $ do
+      let password1 = Spake2.makePassword "abc"
+      let password2 = Spake2.makePassword "cba"
+      let hashAlg = SHA256
+      let group = Ed25519
+      let m = Group.arbitraryElement group ("M" :: ByteString)
+      let n = Group.arbitraryElement group ("N" :: ByteString)
+      let idA = Spake2.SideID ""
+      let idB = Spake2.SideID ""
+      let protocolA = Spake2.makeAsymmetricProtocol hashAlg group m n idA idB Spake2.SideA
+      let protocolB = Spake2.makeAsymmetricProtocol hashAlg group m n idA idB Spake2.SideB
+      sideA <- Spake2.startSpake2 protocolA password1
+      sideB <- Spake2.startSpake2 protocolB password2
+      let aOut = Spake2.computeOutboundMessage sideA
+      let bOut = Spake2.computeOutboundMessage sideB
+      let aKey = Spake2.generateKeyMaterial sideA bOut
+      let bKey = Spake2.generateKeyMaterial sideB aOut
+      let aSessionKey = Spake2.createSessionKey protocolA aOut bOut aKey password1
+      let bSessionKey = Spake2.createSessionKey protocolA aOut bOut bKey password2
+      aSessionKey `shouldNotBe` bSessionKey
+  describe "Symmetric protocol" $ do
+    it "Produces matching session keys when passwords match" $ do
+      let password = Spake2.makePassword "abc"
+      let hashAlg = SHA256
+      let group = Ed25519
+      let s = Group.arbitraryElement group ("M" :: ByteString)
+      let idSymmetric = Spake2.SideID ""
+      let protocol1 = Spake2.makeSymmetricProtocol hashAlg group s idSymmetric
+      let protocol2 = Spake2.makeSymmetricProtocol hashAlg group s idSymmetric
+      side1 <- Spake2.startSpake2 protocol1 password
+      side2 <- Spake2.startSpake2 protocol2 password
+      let out1 = Spake2.computeOutboundMessage side1
+      let out2 = Spake2.computeOutboundMessage side2
+      let key1 = Spake2.generateKeyMaterial side1 out2
+      let key2 = Spake2.generateKeyMaterial side2 out1
+      let sessionKey1 = Spake2.createSessionKey protocol1 out1 out2 key1 password
+      let sessionKey2 = Spake2.createSessionKey protocol2 out1 out2 key2 password
+      sessionKey1 `shouldBe` sessionKey2
+    it "Produces differing session keys when passwords do not match" $ do
+      let password1 = Spake2.makePassword "abc"
+      let password2 = Spake2.makePassword "cba"
+      let hashAlg = SHA256
+      let group = Ed25519
+      let s = Group.arbitraryElement group ("M" :: ByteString)
+      let idSymmetric = Spake2.SideID ""
+      let protocol1 = Spake2.makeSymmetricProtocol hashAlg group s idSymmetric
+      let protocol2 = Spake2.makeSymmetricProtocol hashAlg group s idSymmetric
+      side1 <- Spake2.startSpake2 protocol1 password1
+      side2 <- Spake2.startSpake2 protocol2 password2
+      let out1 = Spake2.computeOutboundMessage side1
+      let out2 = Spake2.computeOutboundMessage side2
+      let key1 = Spake2.generateKeyMaterial side1 out2
+      let key2 = Spake2.generateKeyMaterial side2 out1
+      let sessionKey1 = Spake2.createSessionKey protocol1 out1 out2 key1 password1
+      let sessionKey2 = Spake2.createSessionKey protocol2 out1 out2 key2 password2
+      sessionKey1 `shouldNotBe` sessionKey2
