diff --git a/Assistant/Pairing.hs b/Assistant/Pairing.hs index f384895bd4..8a9d897ebf 100644 --- a/Assistant/Pairing.hs +++ b/Assistant/Pairing.hs @@ -21,14 +21,20 @@ data PairReq = PairReq (Verifiable PairData) data PairAck = PairAck (Verifiable PairData) deriving (Eq, Read, Show) +fromPairReq :: PairReq -> Verifiable PairData +fromPairReq (PairReq v) = v + +fromPairAck :: PairAck -> Verifiable PairData +fromPairAck (PairAck v) = v + data PairMsg = PairReqM PairReq | PairAckM PairAck deriving (Eq, Read, Show) data PairData = PairData - { hostName :: HostName - , userName :: UserName + { remoteHostName :: HostName + , remoteUserName :: UserName , sshPubKey :: Maybe SshPubKey } deriving (Eq, Read, Show) diff --git a/Assistant/WebApp/Configurators/Pairing.hs b/Assistant/WebApp/Configurators/Pairing.hs index f555b29055..82f413a008 100644 --- a/Assistant/WebApp/Configurators/Pairing.hs +++ b/Assistant/WebApp/Configurators/Pairing.hs @@ -2,7 +2,7 @@ - - Pairing works like this: - - - * The user optns StartPairR, which prompts them for a secret. + - * The user opens StartPairR, which prompts them for a secret. - * The user submits it. A PairReq is broadcast out. The secret is - stashed away in a list of known pairing secrets. - * On another device, it's received, and that causes its webapp to @@ -29,23 +29,84 @@ module Assistant.WebApp.Configurators.Pairing where import Assistant.Common import Assistant.Pairing +import Utility.Verifiable import Assistant.WebApp import Assistant.WebApp.Types import Assistant.WebApp.SideBar import Utility.Yesod -import Assistant.WebApp.Configurators.Local -import qualified Types.Remote as R -import qualified Remote.Rsync as Rsync -import qualified Command.InitRemote -import Logs.UUID -import Logs.Remote import Yesod import Data.Text (Text) import qualified Data.Text as T +import qualified Data.Text.Encoding as T +import qualified Data.ByteString.Lazy as B +import Data.Char +import System.Posix.User getStartPairR :: Handler RepHtml -getStartPairR = undefined +getStartPairR = bootstrap (Just Config) $ do + sideBarDisplay + setTitle "Pairing" + promptSecret Nothing $ error "TODO" getFinishPairR :: PairReq -> Handler RepHtml -getFinishPairR = undefined +getFinishPairR req = bootstrap (Just Config) $ do + sideBarDisplay + setTitle "Pairing" + promptSecret (Just req) $ error "TODO" + +data InputSecret = InputSecret { secretText :: Maybe Text } + +promptSecret :: Maybe PairReq -> Widget -> Widget +promptSecret req cont = do + ((result, form), enctype) <- lift $ + runFormGet $ renderBootstrap $ + InputSecret <$> aopt textField "Secret phrase" Nothing + case result of + FormSuccess v -> do + let secret = toSecret $ fromMaybe "" $ secretText v + case req of + Nothing -> case secretProblem secret of + Nothing -> cont + Just problem -> + showform form enctype $ Just problem + Just r -> + if verified (fromPairReq r) secret + then cont + else showform form enctype $ Just + "That's not the right secret phrase." + _ -> showform form enctype Nothing + where + showform form enctype mproblem = do + let start = isNothing req + let badphrase = isJust mproblem + let msg = fromMaybe "" mproblem + let (username, hostname) = maybe ("", "") + (\v -> (T.pack $ remoteUserName v, T.pack $ remoteHostName v)) + (verifiableVal . fromPairReq <$> req) + u <- liftIO $ T.pack . userName + <$> (getUserEntryForID =<< getEffectiveUserID) + let sameusername = username == u + let authtoken = webAppFormAuthToken + $(widgetFile "configurators/pairing") + +{- This counts unicode characters as more than one character, + - but that's ok; they *do* provide additional entropy. -} +secretProblem :: Secret -> Maybe Text +secretProblem s + | B.null s = Just "The secret phrase cannot be left empty. (Remember that punctuation and white space is ignored.)" + | B.length s < 7 = Just "Enter a longer secret phrase, at least 6 characters, but really, a phrase is best! This is not a password you'll need to enter every day." + | s == toSecret sampleQuote = Just "Speaking of foolishness, don't paste in the example I gave. Enter a different phrase, please!" + | otherwise = Nothing + +toSecret :: Text -> Secret +toSecret s = B.fromChunks [T.encodeUtf8 $ T.toLower $ T.filter isAlphaNum s] + +{- From Dickens -} +sampleQuote :: Text +sampleQuote = T.unwords + [ "It was the best of times," + , "it was the worst of times," + , "it was the age of wisdom," + , "it was the age of foolishness." + ] diff --git a/Utility/Verifiable.hs b/Utility/Verifiable.hs index 58218db2a5..b177787c48 100644 --- a/Utility/Verifiable.hs +++ b/Utility/Verifiable.hs @@ -16,8 +16,8 @@ type HMACDigest = String {- A value, verifiable using a HMAC digest and a secret. -} data Verifiable a = Verifiable - { val :: a - , digest :: HMACDigest + { verifiableVal :: a + , verifiableDigest :: HMACDigest } deriving (Eq, Read, Show) @@ -25,7 +25,7 @@ mkVerifiable :: Show a => a -> Secret -> Verifiable a mkVerifiable a secret = Verifiable a (calcDigest (show a) secret) verified :: (Eq a, Show a) => Verifiable a -> Secret -> Bool -verified v secret = v == mkVerifiable (val v) secret +verified v secret = v == mkVerifiable (verifiableVal v) secret calcDigest :: String -> Secret -> HMACDigest calcDigest v secret = showDigest $ hmacSha1 secret $ fromString v diff --git a/templates/configurators/pairing.hamlet b/templates/configurators/pairing.hamlet new file mode 100644 index 0000000000..4aa1cdbb01 --- /dev/null +++ b/templates/configurators/pairing.hamlet @@ -0,0 +1,50 @@ +<div .span9 .hero-unit> + <h2> + Pairing with a local computer + <p> + $if start + Pair with a computer on your local network (or VPN), and the # + two git annex repositories will be combined into one, with changes # + kept in sync between all paired devices. + $else + Pairing with #{username}@#{hostname} will combine the two git annex # + repositories into one, with changes kept in sync between them. + <p> + $if start + For security, enter a secret phrase. This same secret phrase will # + also need to be entered on the computer you're pairing with. # + It will be used to verify you're pairing with the right computer. + $else + $if sameusername + For security, you need to enter the same secret phrase that was # + entered on #{hostname} when the pairing was started. + $else + For security, a secret phrase has been selected, which you need # + to enter here to complete the pairing. If you don't know the # + phrase, go ask #{username} ... + $if badphrase + <div .alert .alert-error> + <i .icon-warning-sign></i> #{msg} + <p> + <form .form-horizontal enctype=#{enctype}> + <fieldset> + ^{form} + ^{authtoken} + <div .form-actions> + <button .btn .btn-primary type=submit> + $if start + Start pairing + $else + Complete pairing + <div .alert .alert-info> + $if start + <p> + A good secret phrase is reasonably long. You'll only # + type it a few times. Only letters and numbers matter; # + punctuation and white space is ignored. + <p> + A quotation is one good choice, something like: # + "#{sampleQuote}" + $else + Only letters and numbers matter; punctuation and white space is # + ignored. diff --git a/templates/configurators/repositories.hamlet b/templates/configurators/repositories.hamlet index d33a1554b9..a38ec10afd 100644 --- a/templates/configurators/repositories.hamlet +++ b/templates/configurators/repositories.hamlet @@ -26,7 +26,8 @@ between computers. <h3> - <i .icon-plus-sign></i> Local computer + <a href="@{StartPairR}"> + <i .icon-plus-sign></i> Local computer <p> Pair with a local computer to automatically keep files in sync # between computers on your local network.