Skip to content

Commit 1732e60

Browse files
authored
Move functions to encode/decode EC keys to Crypto module (#1662)
1 parent 1007deb commit 1732e60

File tree

6 files changed

+70
-62
lines changed

6 files changed

+70
-62
lines changed

src/Simplex/Messaging/Crypto.hs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,13 +87,17 @@ module Simplex.Messaging.Crypto
8787
signatureKeyPair,
8888
publicToX509,
8989
encodeASNObj,
90+
readECPrivateKey,
9091

9192
-- * key encoding/decoding
9293
encodePubKey,
9394
decodePubKey,
9495
encodePrivKey,
9596
decodePrivKey,
9697
pubKeyBytes,
98+
uncompressEncodePoint,
99+
uncompressDecodePoint,
100+
uncompressDecodePrivateNumber,
97101

98102
-- * sign/verify
99103
Signature (..),
@@ -252,6 +256,12 @@ import Simplex.Messaging.Encoding
252256
import Simplex.Messaging.Encoding.String
253257
import Simplex.Messaging.Parsers (parseAll, parseString)
254258
import Simplex.Messaging.Util ((<$?>))
259+
import qualified Crypto.PubKey.ECC.ECDSA as ECDSA
260+
import qualified Crypto.Store.PKCS8 as PK
261+
import qualified Crypto.PubKey.ECC.Types as ECC
262+
import qualified Data.ByteString.Lazy as BL
263+
import qualified Data.Binary as Bin
264+
import qualified Data.Bits as Bits
255265

256266
-- | Cryptographic algorithms.
257267
data Algorithm = Ed25519 | Ed448 | X25519 | X448
@@ -1542,3 +1552,56 @@ keyError :: (a, [ASN1]) -> Either String b
15421552
keyError = \case
15431553
(_, []) -> Left "unknown key algorithm"
15441554
_ -> Left "more than one key"
1555+
1556+
readECPrivateKey :: FilePath -> IO ECDSA.PrivateKey
1557+
readECPrivateKey f = do
1558+
-- this pattern match is specific to APNS key type, it may need to be extended for other push providers
1559+
[PK.Unprotected (X.PrivKeyEC X.PrivKeyEC_Named {privkeyEC_name, privkeyEC_priv})] <- PK.readKeyFile f
1560+
pure ECDSA.PrivateKey {private_curve = ECC.getCurveByName privkeyEC_name, private_d = privkeyEC_priv}
1561+
1562+
-- | Elliptic-Curve-Point-to-Octet-String Conversion without compression
1563+
-- | as required by RFC8291
1564+
-- | https://www.secg.org/sec1-v2.pdf#subsubsection.2.3.3
1565+
uncompressEncodePoint :: ECC.Point -> BL.ByteString
1566+
uncompressEncodePoint (ECC.Point x y) = "\x04" <> encodeBigInt x <> encodeBigInt y
1567+
uncompressEncodePoint ECC.PointO = "\0"
1568+
1569+
uncompressDecodePoint :: BL.ByteString -> Either CE.CryptoError ECC.Point
1570+
uncompressDecodePoint "\0" = pure ECC.PointO
1571+
uncompressDecodePoint s
1572+
| BL.take 1 s /= prefix = Left CE.CryptoError_PointFormatUnsupported
1573+
| BL.length s /= 65 = Left CE.CryptoError_KeySizeInvalid
1574+
| otherwise = do
1575+
let s' = BL.drop 1 s
1576+
x <- decodeBigInt $ BL.take 32 s'
1577+
y <- decodeBigInt $ BL.drop 32 s'
1578+
pure $ ECC.Point x y
1579+
where
1580+
prefix = "\x04" :: BL.ByteString
1581+
1582+
-- Used to test encryption against the RFC8291 Example - which gives the AS private key
1583+
uncompressDecodePrivateNumber :: BL.ByteString -> Either CE.CryptoError ECC.PrivateNumber
1584+
uncompressDecodePrivateNumber s
1585+
| BL.length s /= 32 = Left CE.CryptoError_KeySizeInvalid
1586+
| otherwise = do
1587+
decodeBigInt s
1588+
1589+
encodeBigInt :: Integer -> BL.ByteString
1590+
encodeBigInt i = do
1591+
let s1 = Bits.shiftR i 64
1592+
s2 = Bits.shiftR s1 64
1593+
s3 = Bits.shiftR s2 64
1594+
Bin.encode (w64 s3, w64 s2, w64 s1, w64 i)
1595+
where
1596+
w64 :: Integer -> Bin.Word64
1597+
w64 = fromIntegral
1598+
1599+
decodeBigInt :: BL.ByteString -> Either CE.CryptoError Integer
1600+
decodeBigInt s
1601+
| BL.length s /= 32 = Left CE.CryptoError_PointSizeInvalid
1602+
| otherwise = do
1603+
let (w3, w2, w1, w0) = Bin.decode s :: (Bin.Word64, Bin.Word64, Bin.Word64, Bin.Word64 )
1604+
pure $ shift 3 w3 + shift 2 w2 + shift 1 w1 + shift 0 w0
1605+
where
1606+
shift i w = Bits.shiftL (fromIntegral w) (64 * i)
1607+

src/Simplex/Messaging/Notifications/Protocol.hs

Lines changed: 2 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -484,57 +484,11 @@ data WPKey = WPKey
484484
}
485485
deriving (Eq, Ord, Show)
486486

487-
-- | Elliptic-Curve-Point-to-Octet-String Conversion without compression
488-
-- | as required by RFC8291
489-
-- | https://www.secg.org/sec1-v2.pdf#subsubsection.2.3.3
490-
uncompressEncodePoint :: ECC.Point -> BL.ByteString
491-
uncompressEncodePoint (ECC.Point x y) = "\x04" <> encodeBigInt x <> encodeBigInt y
492-
uncompressEncodePoint ECC.PointO = "\0"
493-
494-
uncompressDecodePoint :: BL.ByteString -> Either CE.CryptoError ECC.Point
495-
uncompressDecodePoint "\0" = pure ECC.PointO
496-
uncompressDecodePoint s
497-
| BL.take 1 s /= prefix = Left CE.CryptoError_PointFormatUnsupported
498-
| BL.length s /= 65 = Left CE.CryptoError_KeySizeInvalid
499-
| otherwise = do
500-
let s' = BL.drop 1 s
501-
x <- decodeBigInt $ BL.take 32 s'
502-
y <- decodeBigInt $ BL.drop 32 s'
503-
pure $ ECC.Point x y
504-
where
505-
prefix = "\x04" :: BL.ByteString
506-
507-
-- Used to test encryption against the RFC8291 Example - which gives the AS private key
508-
uncompressDecodePrivateNumber :: BL.ByteString -> Either CE.CryptoError ECC.PrivateNumber
509-
uncompressDecodePrivateNumber s
510-
| BL.length s /= 32 = Left CE.CryptoError_KeySizeInvalid
511-
| otherwise = do
512-
decodeBigInt s
513-
514487
uncompressEncode :: WPP256dh -> BL.ByteString
515-
uncompressEncode (WPP256dh p) = uncompressEncodePoint p
488+
uncompressEncode (WPP256dh p) = C.uncompressEncodePoint p
516489

517490
uncompressDecode :: BL.ByteString -> Either CE.CryptoError WPP256dh
518-
uncompressDecode bs = WPP256dh <$> uncompressDecodePoint bs
519-
520-
encodeBigInt :: Integer -> BL.ByteString
521-
encodeBigInt i = do
522-
let s1 = Bits.shiftR i 64
523-
s2 = Bits.shiftR s1 64
524-
s3 = Bits.shiftR s2 64
525-
Bin.encode (w64 s3, w64 s2, w64 s1, w64 i)
526-
where
527-
w64 :: Integer -> Bin.Word64
528-
w64 = fromIntegral
529-
530-
decodeBigInt :: BL.ByteString -> Either CE.CryptoError Integer
531-
decodeBigInt s
532-
| BL.length s /= 32 = Left CE.CryptoError_PointSizeInvalid
533-
| otherwise = do
534-
let (w3, w2, w1, w0) = Bin.decode s :: (Bin.Word64, Bin.Word64, Bin.Word64, Bin.Word64 )
535-
pure $ shift 3 w3 + shift 2 w2 + shift 1 w1 + shift 0 w0
536-
where
537-
shift i w = Bits.shiftL (fromIntegral w) (64 * i)
491+
uncompressDecode bs = WPP256dh <$> C.uncompressDecodePoint bs
538492

539493
data WPTokenParams = WPTokenParams
540494
{ wpPath :: ByteString,

src/Simplex/Messaging/Notifications/Server/Push.hs

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ module Simplex.Messaging.Notifications.Server.Push where
1212

1313
import Crypto.Hash.Algorithms (SHA256 (..))
1414
import qualified Crypto.PubKey.ECC.ECDSA as EC
15-
import qualified Crypto.PubKey.ECC.Types as ECT
16-
import qualified Crypto.Store.PKCS8 as PK
1715
import Data.ASN1.BinaryEncoding (DER (..))
1816
import Data.ASN1.Encoding
1917
import Data.ASN1.Types
@@ -27,7 +25,6 @@ import Data.Int (Int64)
2725
import Data.List.NonEmpty (NonEmpty (..))
2826
import Data.Text (Text)
2927
import Data.Time.Clock.System
30-
import qualified Data.X509 as X
3128
import Simplex.Messaging.Notifications.Protocol
3229
import Simplex.Messaging.Parsers (defaultJSON)
3330
import Simplex.Messaging.Transport.HTTP2.Client (HTTP2ClientError)
@@ -74,12 +71,6 @@ signedJWTToken pk (JWTToken hdr claims) = do
7471
jwtEncode = U.encodeUnpadded . LB.toStrict . J.encode
7572
serialize sig = U.encodeUnpadded $ encodeASN1' DER [Start Sequence, IntVal (EC.sign_r sig), IntVal (EC.sign_s sig), End Sequence]
7673

77-
readECPrivateKey :: FilePath -> IO EC.PrivateKey
78-
readECPrivateKey f = do
79-
-- this pattern match is specific to APNS key type, it may need to be extended for other push providers
80-
[PK.Unprotected (X.PrivKeyEC X.PrivKeyEC_Named {privkeyEC_name, privkeyEC_priv})] <- PK.readKeyFile f
81-
pure EC.PrivateKey {private_curve = ECT.getCurveByName privkeyEC_name, private_d = privkeyEC_priv}
82-
8374
data PushNotification
8475
= PNVerification NtfRegCode
8576
| PNMessage (NonEmpty PNMessageData)

src/Simplex/Messaging/Notifications/Server/Push/APNS.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ createAPNSPushClient :: HostName -> APNSPushClientConfig -> IO APNSPushClient
160160
createAPNSPushClient apnsHost apnsCfg@APNSPushClientConfig {authKeyFileEnv, authKeyAlg, authKeyIdEnv, appTeamId} = do
161161
https2Client <- newTVarIO Nothing
162162
void $ connectHTTPS2 apnsHost apnsCfg https2Client
163-
privateKey <- readECPrivateKey =<< getEnv authKeyFileEnv
163+
privateKey <- C.readECPrivateKey =<< getEnv authKeyFileEnv
164164
authKeyId <- T.pack <$> getEnv authKeyIdEnv
165165
let jwtHeader = JWTHeader {alg = authKeyAlg, kid = authKeyId}
166166
jwtToken <- newTVarIO =<< mkApnsJWTToken appTeamId jwtHeader privateKey

src/Simplex/Messaging/Notifications/Server/Push/WebPush.hs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ module Simplex.Messaging.Notifications.Server.Push.WebPush where
1010

1111
import Network.HTTP.Client
1212
import qualified Simplex.Messaging.Crypto as C
13-
import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), WPAuth (..), WPKey (..), WPTokenParams (..), WPP256dh (..), uncompressEncodePoint, wpRequest)
13+
import Simplex.Messaging.Notifications.Protocol (DeviceToken (..), WPAuth (..), WPKey (..), WPTokenParams (..), WPP256dh (..), wpRequest)
1414
import Simplex.Messaging.Notifications.Server.Store.Types
1515
import Simplex.Messaging.Notifications.Server.Push
1616
import Control.Monad.Except
@@ -72,8 +72,8 @@ wpEncrypt wpKey clearT = do
7272
-- | https://www.rfc-editor.org/rfc/rfc8291#section-3.4
7373
wpEncrypt' :: WPKey -> ECC.PrivateNumber -> B.ByteString -> B.ByteString -> ExceptT C.CryptoError IO B.ByteString
7474
wpEncrypt' WPKey {wpAuth, wpP256dh = WPP256dh uaPubK} asPrivK salt clearT = do
75-
let uaPubKS = BL.toStrict . uncompressEncodePoint $ uaPubK
76-
let asPubKS = BL.toStrict . uncompressEncodePoint . ECDH.calculatePublic (ECC.getCurveByName ECC.SEC_p256r1) $ asPrivK
75+
let uaPubKS = BL.toStrict . C.uncompressEncodePoint $ uaPubK
76+
let asPubKS = BL.toStrict . C.uncompressEncodePoint . ECDH.calculatePublic (ECC.getCurveByName ECC.SEC_p256r1) $ asPrivK
7777
ecdhSecret = ECDH.getShared (ECC.getCurveByName ECC.SEC_p256r1) asPrivK uaPubK
7878
prkKey = hmac (unWPAuth wpAuth) ecdhSecret
7979
keyInfo = "WebPush: info\0" <> uaPubKS <> asPubKS

tests/NtfWPTests.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ testWPEncryption = do
6565
let pParams :: WPTokenParams = either error id $ strDecode "/push/JzLQ3raZJfFBR0aqvOMsLrt54w4rJUsV BTBZMqHH6r4Tts7J_aSIgg BCVxsr7N_eNgVRqvHtD0zTZsEc6-VV-JvLexhqUzORcxaOzi6-AYWXvTBHm4bjyPjs7Vd8pZGH6SRpkNtoIAiw4"
6666
let salt :: B.ByteString = either error id $ strDecode "DGv6ra1nlYgDCS1FRnbzlw"
6767
let privBS :: BL.ByteString = either error BL.fromStrict $ strDecode "yfWPiYE-n46HLnH0KqZOF1fJJU3MYrct3AELtAQ-oRw"
68-
asPriv :: ECC.PrivateNumber <- case uncompressDecodePrivateNumber privBS of
68+
asPriv :: ECC.PrivateNumber <- case C.uncompressDecodePrivateNumber privBS of
6969
Left e -> fail $ "Cannot decode PrivateNumber from b64 " <> show e
7070
Right p -> pure p
7171
mCip <- runExceptT $ wpEncrypt' (wpKey pParams) asPriv salt clearT

0 commit comments

Comments
 (0)