Extremely WIP cross-signing and SSSS support

This commit is contained in:
Tulir Asokan
2020-09-12 01:59:53 +03:00
parent 5312ce28d0
commit e7c72dcbbf
8 changed files with 520 additions and 33 deletions

View File

@ -104,6 +104,9 @@ func NewCommandProcessor(parent *MainView) *CommandProcessor {
"e": {"edit"},
"dl": {"download"},
"o": {"open"},
"4s": {"ssss"},
"s4": {"ssss"},
"cs": {"cross-signing"},
},
autocompleters: map[string]CommandAutocompleter{
"devices": autocompleteDevice,
@ -172,6 +175,8 @@ func NewCommandProcessor(parent *MainView) *CommandProcessor {
"import": cmdImportKeys,
"export": cmdExportKeys,
"export-room": cmdExportRoomKeys,
"ssss": cmdSSSS,
"cross-signing": cmdCrossSigning,
},
}
}

View File

@ -19,6 +19,7 @@
package ui
import (
"errors"
"fmt"
"io/ioutil"
"path/filepath"
@ -26,7 +27,10 @@ import (
"time"
"unicode"
ifc "maunium.net/go/gomuks/interface"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/crypto"
"maunium.net/go/mautrix/crypto/ssss"
"maunium.net/go/mautrix/id"
)
@ -243,7 +247,7 @@ func cmdImportKeys(cmd *Command) {
cmd.Reply("Failed to read %s: %v", path, err)
return
}
passphrase, ok := cmd.MainView.AskPassword("Key import", false)
passphrase, ok := cmd.MainView.AskPassword("Key import", "passphrase", "", false)
if !ok {
cmd.Reply("Passphrase entry cancelled")
return
@ -263,7 +267,7 @@ func exportKeys(cmd *Command, sessions []*crypto.InboundGroupSession) {
cmd.Reply("Failed to get absolute path: %v", err)
return
}
passphrase, ok := cmd.MainView.AskPassword("Key export", true)
passphrase, ok := cmd.MainView.AskPassword("Key export", "passphrase", "", true)
if !ok {
cmd.Reply("Passphrase entry cancelled")
return
@ -299,3 +303,341 @@ func cmdExportRoomKeys(cmd *Command) {
}
exportKeys(cmd, sessions)
}
const ssssHelp = `Usage: /%s <subcommand> [...]
Subcommands:
* status [key ID] - Check the status of your SSSS.
* generate [--set-default] - Generate a SSSS key and optionally set it as the default.
* set-default <key ID> - Set a SSSS key as the default.`
func cmdSSSS(cmd *Command) {
if len(cmd.Args) == 0 {
cmd.Reply(ssssHelp, cmd.OrigCommand)
return
}
mach := cmd.Matrix.Crypto().(*crypto.OlmMachine)
switch strings.ToLower(cmd.Args[0]) {
case "status":
keyID := ""
if len(cmd.Args) > 1 {
keyID = cmd.Args[1]
}
cmdS4Status(cmd, mach, keyID)
case "generate":
setDefault := len(cmd.Args) > 1 && strings.ToLower(cmd.Args[1]) == "--set-default"
cmdS4Generate(cmd, mach, setDefault)
case "set-default":
if len(cmd.Args) < 2 {
cmd.Reply("Usage: /%s set-default <key ID>", cmd.OrigCommand)
return
}
cmdS4SetDefault(cmd, mach, cmd.Args[1])
default:
cmd.Reply(ssssHelp, cmd.OrigCommand)
}
}
func cmdS4Status(cmd *Command, mach *crypto.OlmMachine, keyID string) {
var keyData *ssss.KeyMetadata
var err error
if len(keyID) == 0 {
keyID, keyData, err = mach.SSSS.GetDefaultKeyData()
} else {
keyData, err = mach.SSSS.GetKeyData(keyID)
}
if errors.Is(err, ssss.ErrNoDefaultKeyAccountDataEvent) {
cmd.Reply("SSSS is not set up: no default key set")
} else if err != nil {
cmd.Reply("Failed to get key data: %v", err)
return
}
hasPassphrase := "no"
if keyData.Passphrase != nil {
hasPassphrase = fmt.Sprintf("yes (alg=%s,bits=%d,iter=%d)", keyData.Passphrase.Algorithm, keyData.Passphrase.Bits, keyData.Passphrase.Iterations)
}
algorithm := keyData.Algorithm
if algorithm != ssss.AlgorithmAESHMACSHA2 {
algorithm += " (not supported!)"
}
cmd.Reply("Default key is set.\n Key ID: %s\n Has passphrase: %s\n Algorithm: %s", keyID, hasPassphrase, algorithm)
}
func cmdS4Generate(cmd *Command, mach *crypto.OlmMachine, setDefault bool) {
passphrase, ok := cmd.MainView.AskPassword("Passphrase", "", "", false)
if !ok {
return
}
key, err := ssss.NewKey(passphrase)
if err != nil {
cmd.Reply("Failed to generate new key: %v", err)
return
}
err = mach.SSSS.SetKeyData(key.ID, key.Metadata)
if err != nil {
cmd.Reply("Failed to upload key metadata: %v", err)
return
}
// TODO if we start persisting command replies, the recovery key needs to be moved into a popup
cmd.Reply("Successfully generated key %s\nRecovery key: %s", key.ID, key.RecoveryKey())
if setDefault {
err = mach.SSSS.SetDefaultKeyID(key.ID)
if err != nil {
cmd.Reply("Failed to set key as default: %v", err)
}
} else {
cmd.Reply("You can use `/%s set-default %s` to set it as the default", cmd.OrigCommand, key.ID)
}
}
func cmdS4SetDefault(cmd *Command, mach *crypto.OlmMachine, keyID string) {
_, err := mach.SSSS.GetKeyData(keyID)
if err != nil {
if errors.Is(err, mautrix.MNotFound) {
cmd.Reply("Couldn't find key data on server")
} else {
cmd.Reply("Failed to fetch key data: %v", err)
}
return
}
err = mach.SSSS.SetDefaultKeyID(keyID)
if err != nil {
cmd.Reply("Failed to set key as default: %v", err)
} else {
cmd.Reply("Successfully set key %s as default", keyID)
}
}
const crossSigningHelp = `Usage: /%s <subcommand> [...]
Subcommands:
* status
Check the status of your own cross-signing keys.
* generate [--force]
Generate and upload new cross-signing keys.
This will prompt you to enter your account password.
If you already have existing keys, --force is required.
* fetch [--save-to-disk]
Fetch your cross-signing keys from SSSS and decrypt them.
If --save-to-disk is specified, the keys are saved to disk.
* upload
Upload your cross-signing keys to SSSS.`
func cmdCrossSigning(cmd *Command) {
if len(cmd.Args) == 0 {
cmd.Reply(crossSigningHelp, cmd.OrigCommand)
return
}
client := cmd.Matrix.Client()
mach := cmd.Matrix.Crypto().(*crypto.OlmMachine)
switch strings.ToLower(cmd.Args[0]) {
case "status":
cmdCrossSigningStatus(cmd, mach, client)
case "generate":
force := len(cmd.Args) > 1 && strings.ToLower(cmd.Args[1]) == "--force"
cmdCrossSigningGenerate(cmd, cmd.Matrix, mach, client, force)
case "fetch":
saveToDisk := len(cmd.Args) > 1 && strings.ToLower(cmd.Args[1]) == "--save-to-disk"
cmdCrossSigningFetch(cmd, mach, saveToDisk)
case "upload":
cmdCrossSigningUpload(cmd, mach)
default:
cmd.Reply(crossSigningHelp, cmd.OrigCommand)
}
}
func parseKeyResp(keys *mautrix.RespQueryKeys, userID id.UserID) (id.Ed25519, id.Ed25519, id.Ed25519, bool) {
masterKeys, ok := keys.MasterKeys[userID]
if !ok {
return "", "", "", false
}
selfSigningKeys, ok := keys.SelfSigningKeys[userID]
if !ok {
return "", "", "", false
}
userSigningKeys, ok := keys.UserSigningKeys[userID]
if !ok {
return masterKeys.FirstKey(), selfSigningKeys.FirstKey(), "", true
}
return masterKeys.FirstKey(), userSigningKeys.FirstKey(), selfSigningKeys.FirstKey(), true
}
func cmdCrossSigningStatus(cmd *Command, mach *crypto.OlmMachine, client *mautrix.Client) {
if mach.CrossSigningKeys != nil {
cmd.Reply("Cross-signing is set up and private keys are cached")
cmd.Reply("Master key: %s", mach.CrossSigningKeys.MasterKey.PublicKey)
cmd.Reply("User signing key: %s", mach.CrossSigningKeys.UserSigningKey.PublicKey)
cmd.Reply("Self-signing key: %s", mach.CrossSigningKeys.SelfSigningKey.PublicKey)
return
}
keys, err := client.QueryKeys(&mautrix.ReqQueryKeys{
DeviceKeys: mautrix.DeviceKeysRequest{
client.UserID: mautrix.DeviceIDList{client.DeviceID},
},
})
if err != nil {
cmd.Reply("Failed to query own keys: %v", err)
return
}
masterKey, selfSigningKey, userSigningKey, ok := parseKeyResp(keys, client.UserID)
if !ok {
cmd.Reply("Didn't find published cross-signing keys")
return
}
cmd.Reply("Cross-signing is set up, but private keys are not cached")
cmd.Reply("Master key: %s", masterKey)
cmd.Reply("User signing key: %s", userSigningKey)
cmd.Reply("Self-signing key: %s", selfSigningKey)
}
func cmdCrossSigningFetch(cmd *Command, mach *crypto.OlmMachine, saveToDisk bool) {
key := getSSSS(cmd, mach)
if key == nil {
return
}
err := mach.FetchCrossSigningKeysFromSSSS(key)
if err != nil {
cmd.Reply("Error fetching cross-signing keys: %v", err)
return
}
if saveToDisk {
cmd.Reply("Saving keys to disk is not yet implemented")
}
cmd.Reply("Successfully unlocked cross-signing keys")
}
func cmdCrossSigningGenerate(cmd *Command, container ifc.MatrixContainer, mach *crypto.OlmMachine, client *mautrix.Client, force bool) {
if !force {
keys, err := client.QueryKeys(&mautrix.ReqQueryKeys{
DeviceKeys: mautrix.DeviceKeysRequest{
client.UserID: mautrix.DeviceIDList{client.DeviceID},
},
})
if err == nil {
_, _, _, ok := parseKeyResp(keys, client.UserID)
if ok {
cmd.Reply("Found existing cross-signing keys. Use `--force` if you want to overwrite them.")
return
}
}
}
keys, err := mach.GenerateCrossSigningKeys()
if err != nil {
cmd.Reply("Failed to generate cross-signing keys: %v", err)
return
}
err = mach.PublishCrossSigningKeys(keys, func(uia *mautrix.RespUserInteractive) interface{} {
if !uia.HasSingleStageFlow(mautrix.AuthTypePassword) {
for _, flow := range uia.Flows {
if len(flow.Stages) != 1 {
return nil
}
cmd.Reply("Opening browser for authentication")
err := container.UIAFallback(flow.Stages[0], uia.Session)
if err != nil {
cmd.Reply("Authentication failed: %v", err)
return nil
}
return &mautrix.BaseAuthData{
Type: flow.Stages[0],
Session: uia.Session,
}
}
cmd.Reply("No supported authentication mechanisms found")
return nil
}
password, ok := cmd.MainView.AskPassword("Account password", "", "correct horse battery staple", false)
if !ok {
return nil
}
return &mautrix.ReqUIAuthLogin{
BaseAuthData: mautrix.BaseAuthData{
Type: mautrix.AuthTypePassword,
Session: uia.Session,
},
User: mach.Client.UserID.String(),
Password: password,
}
})
if err != nil {
cmd.Reply("Failed to publish cross-signing keys: %v", err)
return
}
mach.CrossSigningKeys = keys
}
func getSSSS(cmd *Command, mach *crypto.OlmMachine) *ssss.Key {
_, keyData, err := mach.SSSS.GetDefaultKeyData()
if err != nil {
if errors.Is(err, mautrix.MNotFound) {
cmd.Reply("SSSS not set up, use `!ssss generate --set-default` first")
} else {
cmd.Reply("Failed to fetch default SSSS key data: %v", err)
}
return nil
}
var key *ssss.Key
if keyData.Passphrase != nil && keyData.Passphrase.Algorithm == ssss.PassphraseAlgorithmPBKDF2 {
passphrase, ok := cmd.MainView.AskPassword("Passphrase", "", "correct horse battery staple", false)
if !ok {
return nil
}
key, err = keyData.VerifyPassphrase(passphrase)
if errors.Is(err, ssss.ErrIncorrectSSSSKey) {
cmd.Reply("Incorrect passphrase")
return nil
}
} else {
recoveryKey, ok := cmd.MainView.AskPassword("Recovery key", "", "tDAK LMRH PiYE bdzi maCe xLX5 wV6P Nmfd c5mC wLef 15Fs VVSc", false)
if !ok {
return nil
}
key, err = keyData.VerifyRecoveryKey(recoveryKey)
if errors.Is(err, ssss.ErrInvalidRecoveryKey) {
cmd.Reply("Malformed recovery key")
return nil
} else if errors.Is(err, ssss.ErrIncorrectSSSSKey) {
cmd.Reply("Incorrect recovery key")
return nil
}
}
// All the errors should already be handled above, this is just for backup
if err != nil {
cmd.Reply("Failed to get SSSS key: %v", err)
return nil
}
return key
}
func cmdCrossSigningUpload(cmd *Command, mach *crypto.OlmMachine) {
if mach.CrossSigningKeys == nil {
cmd.Reply("Cross-signing keys not cached, use `!%s generate` first", cmd.OrigCommand)
return
}
key := getSSSS(cmd, mach)
if key == nil {
return
}
err := mach.UploadCrossSigningKeysToSSSS(key, mach.CrossSigningKeys)
if err != nil {
cmd.Reply("Failed to upload keys to SSSS: %v", err)
} else {
cmd.Reply("Successfully uploaded cross-signing keys to SSSS")
}
}

View File

@ -17,6 +17,9 @@
package ui
import (
"fmt"
"strings"
"maunium.net/go/mauview"
"maunium.net/go/tcell"
)
@ -41,13 +44,20 @@ type PasswordModal struct {
parent *MainView
}
func (view *MainView) AskPassword(title string, isNew bool) (string, bool) {
pwm := NewPasswordModal(view, title, isNew)
func (view *MainView) AskPassword(title, thing, placeholder string, isNew bool) (string, bool) {
pwm := NewPasswordModal(view, title, thing, placeholder, isNew)
view.ShowModal(pwm)
view.parent.Render()
return pwm.Wait()
}
func NewPasswordModal(parent *MainView, title string, isNew bool) *PasswordModal {
func NewPasswordModal(parent *MainView, title, thing, placeholder string, isNew bool) *PasswordModal {
if placeholder == "" {
placeholder = "correct horse battery staple"
}
if thing == "" {
thing = strings.ToLower(title)
}
pwm := &PasswordModal{
parent: parent,
form: mauview.NewForm(),
@ -64,13 +74,13 @@ func NewPasswordModal(parent *MainView, title string, isNew bool) *PasswordModal
pwm.text = mauview.NewTextField()
if isNew {
pwm.text.SetText("Create a passphrase")
pwm.text.SetText(fmt.Sprintf("Create a %s", thing))
} else {
pwm.text.SetText("Enter the passphrase")
pwm.text.SetText(fmt.Sprintf("Enter the %s", thing))
}
pwm.input = mauview.NewInputField().
SetMaskCharacter('*').
SetPlaceholder("correct horse battery staple")
SetPlaceholder(placeholder)
pwm.form.AddComponent(pwm.text, 1, 1, 3, 1)
pwm.form.AddFormItem(pwm.input, 1, 2, 3, 1)
@ -78,10 +88,10 @@ func NewPasswordModal(parent *MainView, title string, isNew bool) *PasswordModal
height += 3
pwm.confirmInput = mauview.NewInputField().
SetMaskCharacter('*').
SetPlaceholder("correct horse battery staple").
SetPlaceholder(placeholder).
SetChangedFunc(pwm.HandleChange)
pwm.input.SetChangedFunc(pwm.HandleChange)
pwm.confirmText = mauview.NewTextField().SetText("Confirm passphrase")
pwm.confirmText = mauview.NewTextField().SetText(fmt.Sprintf("Confirm %s", thing))
pwm.form.SetRow(3, 1).SetRow(4, 1).SetRow(5, 1)
pwm.form.AddComponent(pwm.confirmText, 1, 4, 3, 1)
@ -125,9 +135,9 @@ func (pwm *PasswordModal) ClickSubmit() {
func (pwm *PasswordModal) Wait() (string, bool) {
select {
case result := <- pwm.outputChan:
case result := <-pwm.outputChan:
return result, true
case <- pwm.cancelChan:
case <-pwm.cancelChan:
return "", false
}
}