diff --git a/CHANGELOG b/CHANGELOG
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,3 +1,11 @@
+scuttlebutt-types (0.2.0) unstable; urgency=medium
+
+  * Improved Link types.
+  * Added Hash type.
+  * Some API changes.
+
+ -- Joey Hess <id@joeyh.name>  Thu, 05 Oct 2017 17:40:48 -0400
+
 scuttlebutt-types (0.1.0) unstable; urgency=low
 
   * First release, split off from haskell-scuttlebutt.
diff --git a/scuttlebutt-types.cabal b/scuttlebutt-types.cabal
--- a/scuttlebutt-types.cabal
+++ b/scuttlebutt-types.cabal
@@ -1,5 +1,5 @@
 Name: scuttlebutt-types
-Version: 0.1.0
+Version: 0.2.0
 License: BSD3
 License-File: LICENSE
 Author: bkil.hu, Peter Ferenc Hajdu, Joey Hess
@@ -23,16 +23,18 @@
   Exposed-Modules:
     Ssb.Types
     Ssb.Types.Message
-    Ssb.Types.Key
     Ssb.Types.Link
+    Ssb.Types.Key
+    Ssb.Types.Hash
   Build-Depends:
     base (>= 4.7 && < 5),
     bytestring,
-    bytestring-conversion,
     base64-bytestring,
     aeson,
     ed25519,
-    text
+    cryptonite,
+    text,
+    memory
   Default-Language: Haskell2010
 
 Test-Suite scuttlebutt-types-test
@@ -44,10 +46,13 @@
     scuttlebutt-types,
     hspec,
     bytestring,
-    aeson
+    aeson,
+    text
   Other-Modules:
     MessageSpec
+    LinkSpec
     KeySpec
+    HashSpec
   Ghc-Options: -Wall -fno-warn-tabs -threaded -rtsopts -with-rtsopts=-N
   Default-Language: Haskell2010
 
diff --git a/src/Ssb/Types/Hash.hs b/src/Ssb/Types/Hash.hs
new file mode 100644
--- /dev/null
+++ b/src/Ssb/Types/Hash.hs
@@ -0,0 +1,47 @@
+{-# LANGUAGE PackageImports, OverloadedStrings #-}
+
+module Ssb.Types.Hash where
+
+import Data.ByteString
+import qualified Data.ByteString.Lazy as L
+import Data.ByteArray (convert)
+import qualified Data.ByteString.Base64 as B64
+import qualified Data.Text as T
+import "cryptonite" Crypto.Hash as Crypto
+
+newtype Hash = Sha256 { sha256 :: Crypto.Digest Crypto.SHA256 }
+	deriving (Eq, Ord)
+
+instance Show Hash where
+	show = show . formatHash 
+
+-- | Decodes a SHA256 hash, which is base64 encoded.
+parseSha256 :: ByteString -> Maybe Hash
+parseSha256 b = Sha256 <$> either
+	(const Nothing)
+	Crypto.digestFromByteString
+	(B64.decode b)
+
+-- | Formats a hash as a base64 encoded string.
+formatHash :: Hash -> ByteString
+formatHash (Sha256 { sha256 = s }) = B64.encode (convert s)
+
+type HashType = T.Text
+
+-- | A value accompanied with the hash of the ByteString that it was
+-- deserialized from.
+data Hashed v = Hashed
+	{ hashedValue :: v
+	, hashOf :: Hash
+	}
+	deriving (Show, Eq, Ord)
+
+instance Functor Hashed where
+	fmap f h = h { hashedValue = f (hashedValue h) }
+
+calcHashed :: L.ByteString -> (v -> HashType) -> (L.ByteString -> Maybe v) -> Maybe (Hashed v)
+calcHashed b gettype f = do
+	v <- f b
+	case gettype v of
+		"sha256" -> Just $ Hashed v $ Sha256 (hashlazy b)
+		_ -> Nothing
diff --git a/src/Ssb/Types/Key.hs b/src/Ssb/Types/Key.hs
--- a/src/Ssb/Types/Key.hs
+++ b/src/Ssb/Types/Key.hs
@@ -1,45 +1,17 @@
-{-# LANGUAGE DeriveGeneric, OverloadedStrings #-}
-
-module Ssb.Types.Key (
-	PublicKey(..),
-	parsePublicKey,
-	parseEd25519PublicKey,
-	formatPublicKey,
-) where
+module Ssb.Types.Key where
 
 import Data.ByteString
-import Data.Text.Encoding (encodeUtf8, decodeUtf8)
+import Data.Text.Encoding (decodeUtf8)
 import qualified Data.Text as T
 import qualified Data.ByteString.Base64 as B64
-import qualified Data.Aeson as Aeson
 import qualified Crypto.Sign.Ed25519 as Ed
-import GHC.Generics
-import Data.Monoid
 
 newtype PublicKey = Ed25519PublicKey { ed25519Key :: Ed.PublicKey }
-	deriving (Eq, Ord, Generic)
-
-instance Aeson.FromJSON PublicKey where
-	parseJSON = Aeson.withText "PublicKey" $ \t ->
-		case parsePublicKey (encodeUtf8 t) of
-			Just pk -> return pk
-			Nothing -> fail ("public key decode failure: " ++ T.unpack t)
-
-instance Aeson.ToJSON PublicKey where
-	toJSON = Aeson.String . decodeUtf8 . formatPublicKey
+	deriving (Eq, Ord)
 
 instance Show PublicKey where
 	show  = T.unpack . decodeUtf8 . formatPublicKey
 
--- | Parses a SSB public key, which is base64 encoded, and has a prefix of
--- "@" and a suffix specifying the type of the key.
-parsePublicKey :: ByteString -> Maybe PublicKey
-parsePublicKey b = do
-	b' <- stripPrefix "@" b
-	case stripSuffix ".ed25519" b' of
-		Just b'' -> parseEd25519PublicKey b''
-		Nothing -> Nothing
-
 -- | Decodes a SSB Ed25519 public key, which is base64 encoded.
 parseEd25519PublicKey :: ByteString -> Maybe PublicKey
 parseEd25519PublicKey b = Ed25519PublicKey <$> maybeEd25519PublicKey
@@ -50,8 +22,7 @@
 		(Just . Ed.PublicKey)
 		decodedPublicKey
 
--- | Formats a SSB public key to a base64 encoded string with a prefix of
--- "@" and suffix specifying the type of the key.
+-- | Formats a SSB public key as a base64 encoded string.
 formatPublicKey :: PublicKey -> ByteString
 formatPublicKey (Ed25519PublicKey { ed25519Key = k }) =
-	"@" <> B64.encode (Ed.unPublicKey k) <> ".ed25519"
+	B64.encode (Ed.unPublicKey k)
diff --git a/src/Ssb/Types/Link.hs b/src/Ssb/Types/Link.hs
--- a/src/Ssb/Types/Link.hs
+++ b/src/Ssb/Types/Link.hs
@@ -1,26 +1,139 @@
--- | TODO newtypes for everything
+{-# LANGUAGE OverloadedStrings #-}
 
-module Ssb.Types.Link where
+module Ssb.Types.Link (
+	Link(..),
+	parseLink,
+	formatLink,
+	FeedLink(..),
+	MessageLink(..),
+	BlobLink(..),
+	IsLink(..),
+) where
 
 import Ssb.Types.Key
+import Ssb.Types.Hash
 
-import Data.Aeson
+import Data.Monoid
+import Data.Text.Encoding (encodeUtf8, decodeUtf8)
+import qualified Data.Aeson as Aeson
+import qualified Data.Aeson.Types as Aeson
+import qualified Data.Text as T
+import qualified Crypto.Sign.Ed25519 as Ed
 
--- | A link to a message.
-type MessageLink = String
+-- | A link to a message, feed, or blob.
+data Link = Link
+	{ linkSigil :: Char
+	-- ^ sigil that starts the link, eg '@'
+	, linkTo :: T.Text
+	-- ^ thing being linked to (base64 encoded)
+	, linkTag :: T.Text
+	-- ^ hash or key algorithm of thing being linked to, eg "sha256"
+	} deriving (Eq, Ord)
 
+instance Show Link where
+	show = T.unpack . formatLink
+
+instance Aeson.FromJSON Link where
+	parseJSON = Aeson.withText "Link" $ either fail pure . parseLink
+
+instance Aeson.ToJSON Link where
+	toJSON = Aeson.String . formatLink
+
+-- | Parses a Link eg
+-- "@LA9HYf5rnUJFHHTklKXLLRyrEytayjbFZRo76Aj/qKs=.ed25519"
+parseLink :: T.Text -> Either String Link
+parseLink t
+	| T.null t = Left "empty link"
+	| otherwise =
+		let (sigil, rest) = T.splitAt 1 t
+		    (linkto, linktag) = T.breakOnEnd "." rest
+		in if T.null linktag
+			then Left "missing linkTag"
+			else if T.null linkto
+				then Left "empty link"
+				else Right $ Link
+					{ linkSigil = T.head sigil
+					, linkTo = T.init linkto
+					, linkTag = linktag
+					}
+
+-- | Formats a Link.
+formatLink :: Link -> T.Text
+formatLink l = T.singleton (linkSigil l) <> linkTo l <> "." <> linkTag l
+
+-- | Class of types that are links.
+class IsLink t where
+	fromLink :: Link -> Either String t
+
+	toLink :: t -> Link
+
+	toJSONLink :: t -> Aeson.Value
+	toJSONLink = Aeson.toJSON . toLink
+
+	fromJSONLink :: Aeson.Value -> Aeson.Parser t
+	fromJSONLink o = do
+		l <- Aeson.parseJSON o
+		either fail pure (fromLink l)
+
 -- | A link to a feed.
 newtype FeedLink = FeedLink { unFeedLink :: PublicKey }
 	deriving (Show, Eq, Ord)
 
-instance FromJSON FeedLink where
-	parseJSON d = FeedLink <$> parseJSON d
+instance IsLink FeedLink where
+	fromLink l
+		| linkSigil l == '@' && linkTag l == "ed25519" =
+			maybe (Left "ed25519 key parse failed")
+				(Right . FeedLink)
+				(parseEd25519PublicKey $ encodeUtf8 $ linkTo l)
+		| otherwise = Left "wrong sigil for feed link"
+	toLink fl = Link '@' linkto "ed25519"
+	  where
+		linkto = decodeUtf8 $ Ed.unPublicKey $ ed25519Key $ unFeedLink fl
 
-instance ToJSON FeedLink where
-	toJSON = toJSON . unFeedLink
+instance Aeson.ToJSON FeedLink where
+	toJSON = toJSONLink
 
+instance Aeson.FromJSON FeedLink where
+	parseJSON = fromJSONLink
+
+-- | A link to a message.
+newtype MessageLink = MessageLink { unMessageLink :: Hash }
+	deriving (Show, Eq, Ord)
+
+instance IsLink MessageLink where
+	fromLink l
+		| linkSigil l == '%' && linkTag l == "sha256" =
+			maybe (Left "sha256 hash parse failed")
+				(Right . MessageLink)
+				(parseSha256 $ encodeUtf8 $ linkTo l)
+		| otherwise = Left "wrong sigil for message link"
+	toLink fl = Link '%' linkto "sha256"
+	  where
+		linkto = decodeUtf8 $ formatHash $ unMessageLink fl
+
+instance Aeson.ToJSON MessageLink where
+	toJSON = toJSONLink
+
+instance Aeson.FromJSON MessageLink where
+	parseJSON = fromJSONLink
+
 -- | A link to a blob.
-type BlobLink = String
+newtype BlobLink = BlobLink { unBlobLink :: Hash }
+	deriving (Show, Eq, Ord)
 
--- | A link to a message, feed, or blob.
-type GenericLink = String
+instance IsLink BlobLink where
+	fromLink l
+		| linkSigil l == '&' && linkTag l == "sha256" =
+			maybe (Left "sha256 hash parse failed")
+				(Right . BlobLink)
+				(parseSha256 $ encodeUtf8 $ linkTo l)
+		| otherwise = Left "wrong sigil for blob link"
+	toLink fl = Link '&' linkto "sha256"
+	  where
+		linkto = decodeUtf8 $ formatHash $ unBlobLink fl
+
+instance Aeson.ToJSON BlobLink where
+	toJSON = toJSONLink
+
+instance Aeson.FromJSON BlobLink where
+	parseJSON = fromJSONLink
diff --git a/src/Ssb/Types/Message.hs b/src/Ssb/Types/Message.hs
--- a/src/Ssb/Types/Message.hs
+++ b/src/Ssb/Types/Message.hs
@@ -2,9 +2,10 @@
 
 module Ssb.Types.Message (
 	Signature,
-	HashType,
 	Message(..),
+	messageLink,
 	AnyContent(..),
+	parseMessage,
 	narrowParse,
 	PrivateContent(..),
 	Post(..),
@@ -17,25 +18,24 @@
 	parseMessageType
 ) where
 
-import Ssb.Types.Key
 import Ssb.Types.Link
+import Ssb.Types.Hash
 
 import GHC.Generics
 import Data.Aeson
 import Data.Aeson.Types
 import Data.Int (Int64)
-import qualified Data.Text as T
 import Data.Maybe
 import Data.List
 import Data.Char
 import Control.Applicative
+import qualified Data.Text as T
+import qualified Data.ByteString.Lazy as L
 
 import Prelude hiding (sequence)
 
 type Signature = String
 
-type HashType = String
-
 data Message a = Message
 	{ previous :: Maybe MessageLink
 	, author :: FeedLink
@@ -52,6 +52,14 @@
 instance Functor Message where
 	fmap f m = m { content = f (content m) }
 
+messageLink :: Hashed (Message a) -> MessageLink
+messageLink hm = MessageLink (hashOf hm)
+
+-- | Parses a JSON formatted message, and accompanies it with the 
+-- hash that was originally used for the message.
+parseMessage :: FromJSON a => L.ByteString -> Maybe (Hashed (Message a))
+parseMessage b = calcHashed b hash decode
+
 -- | Parsing a Message AnyContent allows parsing the message envelope,
 -- regardless of the type of content in the message.
 data AnyContent = AnyContent { fromAnyContent :: Value }
@@ -103,7 +111,7 @@
 
 -- | A generic reference to other feeds, entities, or blobs
 data Mention = Mention
-	{ mentionLink :: GenericLink
+	{ mentionLink :: Link
 	} deriving (Show, Eq, Generic)
 
 instance FromJSON Mention where
@@ -113,7 +121,7 @@
 -- They can be used to set a name or picture for users, files, or messages.
 -- However, they're most commonly published about users.
 data About = About
-	{ about :: GenericLink
+	{ about :: Link
 	, name :: Maybe T.Text
 	, image :: Maybe AboutImage
 	} deriving (Show, Eq, Generic)
@@ -149,7 +157,7 @@
 -- | Vote-messages signal approval about someone or something.
 -- Votes can be on users, messages, or blobs.
 data Vote = Vote
-	{ voteLink :: GenericLink
+	{ voteLink :: Link
 	, voteValue :: Int
 	, voteExpression :: Maybe T.Text
 	} deriving (Show, Eq, Generic)
@@ -179,7 +187,7 @@
 data Pub = Pub
 	{ pubHost :: T.Text
 	, pubPort :: Int
-	, pubKey :: PublicKey
+	, pubKey :: FeedLink
 	} deriving (Show, Eq, Generic)
 
 instance FromJSON Pub where
diff --git a/test/HashSpec.hs b/test/HashSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/HashSpec.hs
@@ -0,0 +1,16 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module HashSpec where
+
+import Ssb.Types.Hash
+import Test.Hspec
+import Data.ByteString
+
+base64sha256 :: ByteString
+base64sha256 = "MPB9vxHO0pvi2ve2wh6Do05ZrV7P6ZjUQ+IEYnzLfTs="
+
+spec :: Spec
+spec = describe "sha256 hash" $ do
+	it "can be deserialized and serialized from base64 bytestrings" $ do
+		let (Just hash) = parseSha256 base64sha256
+		(formatHash hash) `shouldBe` base64sha256
diff --git a/test/KeySpec.hs b/test/KeySpec.hs
--- a/test/KeySpec.hs
+++ b/test/KeySpec.hs
@@ -7,10 +7,10 @@
 import Data.ByteString
 
 base64PublicKey :: ByteString
-base64PublicKey = "@opBinLmYiID9SMEoZJPH60LmduSYAR7J00P7/Gj9vHw=.ed25519"
+base64PublicKey = "opBinLmYiID9SMEoZJPH60LmduSYAR7J00P7/Gj9vHw="
 
 spec :: Spec
 spec = describe "public key" $ do
 	it "can be deserialized and serialized from base64 bytestrings" $ do
-		let (Just publicKey) = parsePublicKey base64PublicKey
+		let (Just publicKey) = parseEd25519PublicKey base64PublicKey
 		(formatPublicKey publicKey) `shouldBe` base64PublicKey
diff --git a/test/LinkSpec.hs b/test/LinkSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/LinkSpec.hs
@@ -0,0 +1,26 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module LinkSpec where
+
+import Ssb.Types.Link
+import Ssb.Types.Key
+import Test.Hspec
+import Data.Text
+import Data.Maybe
+
+feedLink :: Text
+feedLink = "@LA9HYf5rnUJFHHTklKXLLRyrEytayjbFZRo76Aj/qKs=.ed25519"
+
+spec :: Spec
+spec = do
+	describe "Link" $ do
+		it "parses" $ do
+			let (Right l) = parseLink feedLink
+			linkSigil l `shouldBe` '@'
+			linkTo l `shouldBe` "LA9HYf5rnUJFHHTklKXLLRyrEytayjbFZRo76Aj/qKs="
+			linkTag l `shouldBe` "ed25519"
+			formatLink l `shouldBe` feedLink
+	describe "FeedLink" $ do
+		it "parses" $ do
+			let (Right l) = fromLink =<< parseLink feedLink
+			unFeedLink l `shouldBe` fromJust (parseEd25519PublicKey "LA9HYf5rnUJFHHTklKXLLRyrEytayjbFZRo76Aj/qKs=")
diff --git a/test/MessageSpec.hs b/test/MessageSpec.hs
--- a/test/MessageSpec.hs
+++ b/test/MessageSpec.hs
@@ -5,10 +5,11 @@
 
 import Test.Hspec
 import Ssb.Types.Message
-import Ssb.Types.Key
 import Ssb.Types.Link
+import Ssb.Types.Key
+import Ssb.Types.Hash
 import Data.Aeson
-import Data.ByteString.Lazy
+import Data.ByteString.Lazy (ByteString)
 import Data.Maybe
 
 import Prelude hiding (sequence)
@@ -50,7 +51,7 @@
 messageSpec = describe "message" $ do
 	it "extracts message content" $ do
 		let (Just message) = (decode originalMessage) :: Maybe (Message AnyContent)
-		previous message `shouldBe` Just "%3AWRZYdDHKOmLOWvzHbwJFjx9g8hOQH/NXZkwciA63Y=.sha256"
+		previous message `shouldBe` MessageLink <$> parseSha256 "3AWRZYdDHKOmLOWvzHbwJFjx9g8hOQH/NXZkwciA63Y="
 		author message `shouldBe` fromJust (FeedLink <$> parseEd25519PublicKey "EMovhfIrFk4NihAKnRNhrfRaqIhBv1Wj8pTxJNgvCCY=")
 		sequence message `shouldBe` 184
 		timestamp message `shouldBe` 1449954503740
@@ -61,16 +62,16 @@
 postSpec = describe "post" $ do
 	it "extracts message post" $ do
 		let (Just message) = (decode originalMessage) :: Maybe (Message Post)
-		content message `shouldBe` Post
-			{ text = "@johnny use https://github.com/ssbc/ssb-msgs and https://github.com/ssbc/ssb-ref is probably useful too."
-			, channel = Nothing
-			, root = Just "%rf1JvoFg1pHE6TkuuMxsjBNevFck7LQvXGhkLMNlaYs=.sha256"
-			, branch = Just "%G37a4PUTbysiETQazyYATwLkfn/ZzigmIheAx905MLU=.sha256"
-			, recps = Nothing
-			, mentions = Just
-				[ Mention { mentionLink = "@dnr1swLSAgf36g+FzGjNLgmytj2IIyDaYeKZ7F5GdzY=.ed25519" }
-				]
-			}
+		text (content message) `shouldBe`
+			"@johnny use https://github.com/ssbc/ssb-msgs and https://github.com/ssbc/ssb-ref is probably useful too."
+		channel (content message) `shouldBe` Nothing
+		root (content message) `shouldBe`
+			MessageLink <$> parseSha256 "rf1JvoFg1pHE6TkuuMxsjBNevFck7LQvXGhkLMNlaYs="
+		branch (content message) `shouldBe`
+			MessageLink <$> parseSha256 "G37a4PUTbysiETQazyYATwLkfn/ZzigmIheAx905MLU="
+		recps (content message) `shouldBe` Nothing
+		map (linkTo . mentionLink) <$> (mentions (content message)) `shouldBe`
+			Just [ "dnr1swLSAgf36g+FzGjNLgmytj2IIyDaYeKZ7F5GdzY=" ]
 
 	it "does not parse content that is not a post" $ do
 		(decode originalAbout :: Maybe (Message Post)) `shouldBe` Nothing
@@ -101,10 +102,10 @@
 aboutSpec = describe "about" $ do
 	it "extracts message about" $ do
 		let Just message = (decode originalAbout) :: Maybe (Message About)
-		about (content message) `shouldBe` "@4RUNgFpvI4ZHH6mPPC9GLzsdv33Cs2JGMMcs8rJSMuU=.ed25519"
+		linkTo (about (content message)) `shouldBe` "4RUNgFpvI4ZHH6mPPC9GLzsdv33Cs2JGMMcs8rJSMuU="
 		name (content message) `shouldBe` Nothing
 		fmap aboutImageLink (image (content message)) `shouldBe`
-			Just "&Ro1xCJNDIwwKul2SLSU1e4hrwekPcBI6OrBFN0JWnQM=.sha256"
+			BlobLink <$> parseSha256 "Ro1xCJNDIwwKul2SLSU1e4hrwekPcBI6OrBFN0JWnQM="
 		fmap aboutImageSize (image (content message)) `shouldBe`
 			Just (Just 502712)
 		fmap aboutImageType (image (content message)) `shouldBe`
@@ -162,11 +163,9 @@
 voteSpec = describe "vote" $ do
 	it "extracts message vote" $ do
 		let Just message = (decode originalVote) :: Maybe (Message Vote)
-		content message `shouldBe` Vote
-			{ voteLink = "%lu9JsdUl4astKykyaVFdkLb7//tS0K08BMs6dT+fErA=.sha256"
-			, voteValue = 1
-			, voteExpression = Nothing
-			}
+		voteValue (content message) `shouldBe` 1
+		voteExpression (content message) `shouldBe` Nothing
+		linkTo (voteLink (content message)) `shouldBe` "lu9JsdUl4astKykyaVFdkLb7//tS0K08BMs6dT+fErA="
 
 -- Not a real private message content, because the test couldn't decrypt it anyway.
 originalPrivateContent :: ByteString
@@ -213,5 +212,5 @@
 		let Just message = (decode originalPub) :: Maybe (Message Pub)
 		pubHost (content message) `shouldBe` "ssb.hypersignal.xyz"
 		pubPort (content message) `shouldBe` 8008
-		Just (pubKey (content message)) `shouldBe`
+		Just (unFeedLink (pubKey (content message))) `shouldBe`
 			parseEd25519PublicKey "XRg7pXoQqsWDDk4dmgvSWHUqzwS6BmqMo4IdbMKPjWA="
