diff options
author | Joe Crayne <joe@jerkface.net> | 2019-11-14 16:45:14 -0500 |
---|---|---|
committer | Joe Crayne <joe@jerkface.net> | 2019-11-14 16:45:14 -0500 |
commit | b42c0d847a785487f3222b0d5360746d25d3209c (patch) | |
tree | 11ef85e3e4577eb047227f9938761bdac94a1309 /Data/OpenPGP/Util/Cv25519.hs | |
parent | 76bf7e08bccbb1a3a689068016b8a9c29d1e060e (diff) |
Cv25519 encryption.
Diffstat (limited to 'Data/OpenPGP/Util/Cv25519.hs')
-rw-r--r-- | Data/OpenPGP/Util/Cv25519.hs | 231 |
1 files changed, 231 insertions, 0 deletions
diff --git a/Data/OpenPGP/Util/Cv25519.hs b/Data/OpenPGP/Util/Cv25519.hs new file mode 100644 index 0000000..aef3521 --- /dev/null +++ b/Data/OpenPGP/Util/Cv25519.hs | |||
@@ -0,0 +1,231 @@ | |||
1 | {-# LANGUAGE ExistentialQuantification #-} | ||
2 | {-# LANGUAGE QuasiQuotes #-} | ||
3 | module Data.OpenPGP.Util.Cv25519 where | ||
4 | |||
5 | import Control.Arrow | ||
6 | import Control.Monad | ||
7 | import Data.Binary | ||
8 | import Data.Binary.Get | ||
9 | import Data.ByteString (ByteString) | ||
10 | import Data.Bits | ||
11 | import qualified Data.ByteArray as BA | ||
12 | import qualified Data.ByteString.Char8 as B8 | ||
13 | import qualified Data.ByteString as BS | ||
14 | import qualified Data.ByteString.Lazy as BL | ||
15 | import Data.Char | ||
16 | import Numeric | ||
17 | import Data.Int | ||
18 | |||
19 | import Data.OpenPGP.Internal | ||
20 | import Data.OpenPGP.Util | ||
21 | import Data.OpenPGP.Util.Base | ||
22 | import Data.OpenPGP as OpenPGP | ||
23 | import Crypto.Cipher.SBox | ||
24 | import Data.OpenPGP.Util.Ed25519 (zeroExtend,zeroPad) | ||
25 | import qualified Crypto.PubKey.Curve25519 as Cv25519 | ||
26 | import Crypto.Error | ||
27 | import Crypto.Cipher.AES | ||
28 | import Crypto.Cipher.Types | ||
29 | import Data.OpenPGP.Util.DecryptSecretKey -- (withS2K, simpleUnCFB, Enciphered(..)) | ||
30 | |||
31 | import Crypto.JOSE.AESKW | ||
32 | |||
33 | oid_cv25519 = 0x2B060104019755010501 | ||
34 | |||
35 | getEphemeralKey :: OpenPGP.Packet -> Maybe ([(Char,MPI)],BL.ByteString) | ||
36 | getEphemeralKey AsymmetricSessionKeyPacket | ||
37 | { version = 3 | ||
38 | , key_algorithm = ECC | ||
39 | , encrypted_data = dta } = do | ||
40 | -- Algorithm-Specific Fields for ECDH encryption: | ||
41 | -- | ||
42 | -- * MPI of an EC point representing an ephemeral public key. | ||
43 | -- | ||
44 | -- * a one-octet size, followed by a symmetric key encoded using the | ||
45 | -- method described in Section 13.5. | ||
46 | (b,_,d) <- either (const Nothing) Just $ runGetOrFail getEllipticCurvePublicKey dta | ||
47 | (sz,m) <- BL.uncons b | ||
48 | guard $ BL.length m == fromIntegral sz | ||
49 | return (d,m) | ||
50 | getEphemeralKey _ = Nothing | ||
51 | |||
52 | -- The value "m" in the above formulas is derived from the session key | ||
53 | -- as follows. First, the session key is prefixed with a one-octet | ||
54 | -- algorithm identifier that specifies the symmetric encryption | ||
55 | -- algorithm used to encrypt the following Symmetrically Encrypted Data | ||
56 | -- Packet. Then a two-octet checksum is appended, which is equal to the | ||
57 | -- sum of the preceding session key octets, not including the algorithm | ||
58 | -- identifier, modulo 65536. This value is then encoded as described in | ||
59 | -- PKCS#1 block encoding EME-PKCS1-v1_5 in Section 7.2.1 of [RFC3447] to | ||
60 | -- form the "m" value used in the formulas above. See Section 14.1 of | ||
61 | -- this document for notes on OpenPGP's use of PKCS#1. | ||
62 | |||
63 | privateCv25519Key :: OpenPGP.Packet -> Maybe Cv25519.SecretKey | ||
64 | privateCv25519Key k@SecretKeyPacket { key_algorithm = ECC, symmetric_algorithm = Unencrypted } = do | ||
65 | guard $ oid_cv25519 == keyParam 'c' k | ||
66 | case Cv25519.secretKey $ zeroExtend 32 $ integerToLE (keyParam 'd' k) of | ||
67 | CryptoPassed cv25519sec -> Just cv25519sec | ||
68 | CryptoFailed err -> Nothing | ||
69 | |||
70 | hexify = map toUpper . hexString . BS.unpack | ||
71 | |||
72 | |||
73 | |||
74 | hexString :: [Word8] -> String | ||
75 | hexString = foldr (pad `oo` showHex) "" | ||
76 | where | ||
77 | pad s | odd $ length s = '0':s | ||
78 | | otherwise = s | ||
79 | |||
80 | oo :: (b -> c) -> (a -> a1 -> b) -> a -> a1 -> c | ||
81 | oo = (.) . (.) | ||
82 | |||
83 | cv25519Key :: [(Char,MPI)] -> Maybe Cv25519.PublicKey | ||
84 | cv25519Key k = do | ||
85 | MPI flag <- lookup 'f' k | ||
86 | n <- case flag of | ||
87 | 0x40 -> zeroPad 32 . integerToBS . (\(MPI n)-> n) <$> lookup 'n' k | ||
88 | -- TODO: The following was based on Ed25519. Verify that it is correct for Cv25519. | ||
89 | _ -> do MPI y <- lookup 'y' k | ||
90 | MPI x <- lookup 'x' k | ||
91 | let ybs = zeroExtend 32 $ integerToLE y | ||
92 | lb = BS.last ybs | ||
93 | return $ if x < 0 then BS.take 31 ybs `BS.snoc` (lb .|. 0x80) | ||
94 | else BS.take 31 ybs `BS.snoc` (lb .&. 0x7F) | ||
95 | maybeCryptoError $ Cv25519.publicKey n | ||
96 | |||
97 | kdfParams :: OpenPGP.Packet -> (OpenPGP.HashAlgorithm, OpenPGP.SymmetricAlgorithm) | ||
98 | kdfParams k = toEnum *** toEnum $ divMod e 256 | ||
99 | where | ||
100 | e = 0x0FFFF .&. (fromIntegral $ keyParam 'e' k) | ||
101 | -- flen <- get :: Get Word8 -- always 3 (length of following bytes) | ||
102 | -- one <- get :: Get Word8 -- always 0x01 (reserved) | ||
103 | -- hashid <- get :: Get Word8 -- HashAlgorithm | ||
104 | -- algoid <- get :: Get Word8 -- SymmetricAlgorithm | ||
105 | |||
106 | data SomeKeyCipher = forall c. BlockCipher128 c => SomeKeyCipher c | ||
107 | |||
108 | someAES128 :: AES128 -> SomeKeyCipher | ||
109 | someAES192 :: AES192 -> SomeKeyCipher | ||
110 | someAES256 :: AES256 -> SomeKeyCipher | ||
111 | someAES128 = SomeKeyCipher | ||
112 | someAES192 = SomeKeyCipher | ||
113 | someAES256 = SomeKeyCipher | ||
114 | |||
115 | keyCipher :: OpenPGP.SymmetricAlgorithm -> BS.ByteString -> Maybe SomeKeyCipher | ||
116 | keyCipher OpenPGP.AES128 key = someAES128 <$> maybeCryptoError (cipherInit key) | ||
117 | keyCipher OpenPGP.AES192 key = someAES192 <$> maybeCryptoError (cipherInit key) | ||
118 | keyCipher OpenPGP.AES256 key = someAES256 <$> maybeCryptoError (cipherInit key) | ||
119 | keyCipher _ _ = Nothing | ||
120 | |||
121 | keyCipherSize OpenPGP.AES128 = cipherKeySize (undefined :: AES128) | ||
122 | keyCipherSize OpenPGP.AES192 = cipherKeySize (undefined :: AES192) | ||
123 | keyCipherSize OpenPGP.AES256 = cipherKeySize (undefined :: AES256) | ||
124 | |||
125 | |||
126 | kdfParamBytes :: OpenPGP.Packet -> BL.ByteString | ||
127 | kdfParamBytes k = BL.fromChunks | ||
128 | [ BL.toStrict $ encodeOID (MPI $ keyParam 'c' k) -- curve_OID_len || curve_OID | ||
129 | , BS.singleton $ fromIntegral $ fromEnum $ key_algorithm k -- public_key_alg_ID | ||
130 | , BL.toStrict $ encode (fromIntegral (keyParam 'e' k) :: Word32) -- 03 || 01 || KDF_hash_ID || KEK_alg_ID for AESKeyWrap | ||
131 | , B8.pack "Anonymous Sender " | ||
132 | , let Fingerprint fp = fingerprint k in fp | ||
133 | ] | ||
134 | |||
135 | -- The Concatenation Key Derivation Function (Approved Alternative 1) [SP800-56A] | ||
136 | kdf :: OpenPGP.HashAlgorithm -> Cv25519.DhSecret -> Int -> BL.ByteString -> Maybe BL.ByteString | ||
137 | kdf hsh z keybytelen otherinfo | ||
138 | | reps > 2^32 - 1 = Nothing | ||
139 | -- XXX: I don't understand /max_hash_inputlen/. | ||
140 | -- | ||
141 | -- max_hash_inputlen: an integer that indicates the maximum length (in | ||
142 | -- bits) of the bit string(s) input to the hash function. | ||
143 | -- | ||
144 | -- | 8 * (BS.length zo) > max_hash_inputlen - 32 = Nothing | ||
145 | | otherwise = Just derivedKeyingMaterial | ||
146 | where | ||
147 | keydatalen = 8 * fromIntegral keybytelen :: Int64 | ||
148 | hashlen = 8 * fromIntegral (hashLen hsh) :: Int64 | ||
149 | reps = fromIntegral $ (keydatalen + hashlen - 1) `div` hashlen | ||
150 | counter = 0x00000001 :: Word32 | ||
151 | zo = BL.fromStrict (BA.convert z) <> otherinfo | ||
152 | hashes = [ hashBySymbol hsh (encode (i::Word32) <> zo) | ||
153 | | i <- [1 .. reps] ] -- Compute Hash i = H(counter || Z || OtherInfo). | ||
154 | -- Let Hhash be set to Hash[reps] if (keydatalen / hashlen) is an integer; otherwise, let Hhash | ||
155 | -- be set to the (keydatalen mod hashlen) leftmost bits of Hash[reps]. | ||
156 | hhash = case keydatalen `mod` hashlen of | ||
157 | 0 -> last hashes | ||
158 | r -> BS.take (fromIntegral $ (r + 7) `div` 8) $ last hashes -- TODO: Zero out the 8 - (r `mod` 8) last bits? | ||
159 | derivedKeyingMaterial = BL.fromChunks $ init hashes ++ [ hhash ] | ||
160 | |||
161 | |||
162 | -- The input to the key wrapping method is the value "m" derived from | ||
163 | -- the session key, as described in Section 5.1, "Public-Key Encrypted | ||
164 | -- Session Key Packets (Tag 1)", except that the PKCS #1.5 padding step | ||
165 | -- is omitted. The result is padded using the method described in | ||
166 | -- [PKCS5] to the 8-byte granularity. For example, the following | ||
167 | -- AES-256 session key, in which 32 octets are denoted from k0 to k31, | ||
168 | -- is composed to form the following 40 octet sequence: | ||
169 | -- | ||
170 | -- 09 k0 k1 ... k31 c0 c1 05 05 05 05 05 | ||
171 | -- | ||
172 | -- The octets c0 and c1 above denote the checksum. This encoding allows | ||
173 | -- the sender to obfuscate the size of the symmetric encryption key used | ||
174 | -- to encrypt the data. For example, assuming that an AES algorithm is | ||
175 | -- used for the session key, the sender MAY use 21, 13, and 5 bytes of | ||
176 | -- padding for AES-128, AES-192, and AES-256, respectively, to provide | ||
177 | -- the same number of octets, 40 total, as an input to the key wrapping | ||
178 | -- method. | ||
179 | -- | ||
180 | -- From Section 5.1, "Public-Key Encrypted Session Key Packets (Tag 1)" | ||
181 | -- | ||
182 | -- The value "m" in the above formulas is derived from the session key | ||
183 | -- as follows. First, the session key is prefixed with a one-octet | ||
184 | -- algorithm identifier that specifies the symmetric encryption | ||
185 | -- algorithm used to encrypt the following Symmetrically Encrypted Data | ||
186 | -- Packet. Then a two-octet checksum is appended, which is equal to the | ||
187 | -- sum of the preceding session key octets, not including the algorithm | ||
188 | -- identifier, modulo 65536. This value is then encoded as described in | ||
189 | -- PKCS#1 block encoding EME-PKCS1-v1_5 in Section 7.2.1 of [RFC3447] to | ||
190 | -- form the "m" value used in the formulas above. See Section 14.1 of | ||
191 | -- this document for notes on OpenPGP's use of PKCS#1. | ||
192 | decodeEncryptedKey :: ByteString -> Maybe (SymmetricAlgorithm, ByteString) | ||
193 | decodeEncryptedKey m = do | ||
194 | (algb,ks) <- BS.uncons m | ||
195 | let alg = toEnum $ fromIntegral algb :: OpenPGP.SymmetricAlgorithm | ||
196 | sz <- case keyCipherSize alg of | ||
197 | KeySizeFixed n -> Just n | ||
198 | _ -> Nothing | ||
199 | let (key,macbs) = BS.splitAt sz ks | ||
200 | (macb,trail) = BS.splitAt 2 macbs | ||
201 | mac = decode $ BL.fromStrict macb :: Word16 | ||
202 | chk = sum $ map fromIntegral $ BS.unpack key | ||
203 | guard $ chk == mac | ||
204 | Just (alg, key) | ||
205 | |||
206 | decryptMessage :: Packet -- ^ local secret key (ecdh cv25519) | ||
207 | -> Packet -- ^ ephemeral remote public key (ecdh cv25519) and encrypted symmetric key. | ||
208 | -> Packet -- ^ symmetrically encrypted data packet | ||
209 | -> Maybe [Packet] | ||
210 | decryptMessage ecdhkey asym encdta = do | ||
211 | (pubk,m) <- getEphemeralKey asym | ||
212 | pub25519 <- cv25519Key pubk | ||
213 | sec25519 <- privateCv25519Key ecdhkey | ||
214 | let shared = Cv25519.dh pub25519 sec25519 | ||
215 | (hsh, alg) = kdfParams ecdhkey | ||
216 | miv = let sz = case keyCipherSize alg of | ||
217 | KeySizeFixed n -> n | ||
218 | KeySizeEnum ns -> head ns | ||
219 | KeySizeRange mn mx -> mn | ||
220 | in kdf hsh shared sz (kdfParamBytes ecdhkey) | ||
221 | (alg,k) <- do | ||
222 | iv <- BL.toStrict <$> miv | ||
223 | SomeKeyCipher c <- keyCipher alg iv | ||
224 | m' <- aesKeyUnwrap c (BL.toStrict m) :: Maybe BS.ByteString | ||
225 | decodeEncryptedKey m' | ||
226 | withS2K' alg Nothing (BL.fromStrict k) $ \cipher -> do | ||
227 | let blksize = blockSize cipher | ||
228 | b0 = simpleUnCFB cipher nullIV (encrypted_data encdta) | ||
229 | b1 = BL.drop (2 + fromIntegral blksize) b0 | ||
230 | (_,_, Message ps) <- either (const Nothing) Just $ decodeOrFail b1 | ||
231 | return ps | ||