Public Access
Keys sync and keys are shared: SshKey is in the sync registry on both sides, the material rides in the sealed payload, and every generation of the vault key is wrapped to a new member. What was missing was the way in. Hosts and groups could move between vaults and keychain items could not, so a key typed into a personal vault before the team existed stayed there for good — and moving a host into the team's vault left it authenticating with something nobody else in that vault can read. The code said so and could do nothing about it: "the answer is usually to put a copy of that key in the destination vault", which meant pasting the private half into a second item and deleting the first. A private key on a clipboard, and two items nobody can tell apart afterwards. MoveAsync already existed on the generic repository and is now exposed for keys and passwords as it is for hosts and groups. What had to be built around it is the re-aim. An item re-sealed under another vault's key lands with an id of that vault's making, so every host bound to the old one and every group lending it as a default is left naming a tombstone — and a host bound to something its vault no longer holds refuses to connect rather than falling back to a typed password. A move without the re-aim would look like a success and break every machine on that key. It runs over every vault this session can write to, because a binding resolves across all of them, and it counts what it could not rewrite: an item from a newer client, or one in a vault this account may only read. Those are said in the sentence afterwards rather than swallowed. The host's move asks the question rather than deciding it. A binding resolves across vaults, so the moved host goes on working for the person who moved it whichever way this is answered; it is the colleagues they have just joined who hold one vault's key and cannot connect with a host whose key stayed behind. Unticked, and it stays that way on purpose: moving a key into a team's vault hands it to everybody holding that key, and this design does not default anybody into a disclosure. Under the box is the count of everything else that authenticates with that key, because a key twenty machines use is a different decision from one nothing else touches, and neither number is visible from the panel otherwise. The question is answered against the vault in the picker, so choosing a different destination re-asks it and a key already in the destination offers nothing. One thing fixed on the way. A host that inherited its key from its group arrived in the destination naming nothing at all — the group belongs to the vault it left — so a machine that connected before the move refused after it, with no sentence anywhere saying why. The resolved binding is now written onto the host as it crosses, and the stranded-binding warning reads the resolved binding too, which is the case where somebody is least likely to know a key is involved. MOVE is on both heads, for keys and passwords only: a tag, a bucket and a pin are read from the active vault alone, so "another vault" is not a question any of them has. Four tests cover the move and its re-aim, the host's move with the key brought and without it, and the inherited binding.
531 lines
32 KiB
XML
531 lines
32 KiB
XML
<UserControl xmlns="https://github.com/avaloniaui"
|
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
xmlns:vm="using:DodoSSH.Client.Shell.ViewModels"
|
|
xmlns:views="using:DodoSSH.Client.App.Views"
|
|
xmlns:ssh="using:DodoSSH.Client.Ssh"
|
|
x:Class="DodoSSH.Client.App.Views.KeychainScreen"
|
|
x:DataType="vm:VaultViewModel">
|
|
|
|
<!--
|
|
The keychain: the SSH keys and the stored passwords. Things a person creates and edits.
|
|
|
|
Three columns, as the design has them — a category rail, one table, and a detail pane. The table has one
|
|
shape for every kind, which is what makes the ALL category possible and is why the row projection
|
|
exists; see VaultItemRowViewModel.
|
|
|
|
HOST KEYS was a fourth category here and is now a screen of its own; see KnownHostsScreen. It never fit:
|
|
the two categories left are things somebody made on purpose, and a pin is a decision recorded at the
|
|
moment of connecting — nobody goes looking for one in a list of credentials. It also has a workflow the
|
|
shared table could not serve, which is comparing an untruncated fingerprint against a published one.
|
|
|
|
Two of the design's five categories are still not here. IDENTITIES and CERTIFICATES have no item type
|
|
behind them, so listing them would be two headings that could never have anything under them. Recorded
|
|
in docs/design-import-gaps.md.
|
|
|
|
The SCOPES rail below the categories is the keychain list. Since M3 it genuinely has more than one entry
|
|
when somebody is in a team — but it is still not a selector, because every table on this screen already
|
|
spans every keychain this session holds a key for and each row names its own. What it carries instead is
|
|
the one keychain question with an answer: where a new item is filed.
|
|
-->
|
|
|
|
<Grid ColumnDefinitions="176,*,244">
|
|
|
|
<!-- Categories and scopes -->
|
|
<Border Grid.Column="0" Background="{StaticResource Sidebar}"
|
|
BorderBrush="{StaticResource Border}" BorderThickness="0,0,1,0">
|
|
<ScrollViewer>
|
|
<StackPanel Margin="0,12">
|
|
|
|
<TextBlock Classes="label" Text="KEYCHAIN" Margin="14,0,14,8" />
|
|
|
|
<Button Classes="flat cat" Command="{Binding ShowSectionCommand}"
|
|
CommandParameter="{x:Static vm:VaultSection.All}"
|
|
Classes.active="{Binding ShowsAll}">
|
|
<Grid ColumnDefinitions="Auto,*,Auto">
|
|
<Border Grid.Column="0" Classes="rowmark catmark" />
|
|
<TextBlock Grid.Column="1" Text="ALL" Margin="12,0,0,0" />
|
|
<TextBlock Grid.Column="2" Text="{Binding TotalItemCount}"
|
|
Foreground="{StaticResource TextFaint}" />
|
|
</Grid>
|
|
</Button>
|
|
|
|
<Button Classes="flat cat" Command="{Binding ShowSectionCommand}"
|
|
CommandParameter="{x:Static vm:VaultSection.Keys}"
|
|
Classes.active="{Binding ShowsKeys}">
|
|
<Grid ColumnDefinitions="Auto,*,Auto">
|
|
<Border Grid.Column="0" Classes="rowmark catmark" />
|
|
<TextBlock Grid.Column="1" Text="SSH KEYS" Margin="12,0,0,0" />
|
|
<TextBlock Grid.Column="2" Text="{Binding Keys.Count}"
|
|
Foreground="{StaticResource TextFaint}" />
|
|
</Grid>
|
|
</Button>
|
|
|
|
<Button Classes="flat cat" Command="{Binding ShowSectionCommand}"
|
|
CommandParameter="{x:Static vm:VaultSection.Credentials}"
|
|
Classes.active="{Binding ShowsCredentials}">
|
|
<Grid ColumnDefinitions="Auto,*,Auto">
|
|
<Border Grid.Column="0" Classes="rowmark catmark" />
|
|
<TextBlock Grid.Column="1" Text="PASSWORDS" Margin="12,0,0,0" />
|
|
<TextBlock Grid.Column="2" Text="{Binding Credentials.Count}"
|
|
Foreground="{StaticResource TextFaint}" />
|
|
</Grid>
|
|
</Button>
|
|
|
|
<!--
|
|
Buckets. A category here rather than a screen of its own, unlike the approved host keys: a bucket
|
|
is something somebody creates, edits and keeps a secret for, which is what the other two
|
|
categories are. A pin is a decision recorded at connect time and is not.
|
|
-->
|
|
<!--
|
|
Tags. The odd category: it is the only one holding nothing secret — a tag is a name. It is here
|
|
because the reason a tag is an item at all is that renaming it should be one write instead of
|
|
twenty, and a rename needs somewhere to happen; so does deleting, or the host editor's picker
|
|
fills with names nobody uses and never empties.
|
|
-->
|
|
<Button Classes="flat cat" Command="{Binding ShowSectionCommand}"
|
|
CommandParameter="{x:Static vm:VaultSection.Tags}"
|
|
Classes.active="{Binding ShowsTags}">
|
|
<Grid ColumnDefinitions="Auto,*,Auto">
|
|
<Border Grid.Column="0" Classes="rowmark catmark" />
|
|
<TextBlock Grid.Column="1" Text="TAGS" Margin="12,0,0,0" />
|
|
<TextBlock Grid.Column="2" Text="{Binding Tags.Count}"
|
|
Foreground="{StaticResource TextFaint}" />
|
|
</Grid>
|
|
</Button>
|
|
|
|
<Button Classes="flat cat" Command="{Binding ShowSectionCommand}"
|
|
CommandParameter="{x:Static vm:VaultSection.Buckets}"
|
|
Classes.active="{Binding ShowsBuckets}">
|
|
<Grid ColumnDefinitions="Auto,*,Auto">
|
|
<Border Grid.Column="0" Classes="rowmark catmark" />
|
|
<TextBlock Grid.Column="1" Text="BUCKETS" Margin="12,0,0,0" />
|
|
<TextBlock Grid.Column="2" Text="{Binding ObjectStores.Count}"
|
|
Foreground="{StaticResource TextFaint}" />
|
|
</Grid>
|
|
</Button>
|
|
|
|
<Border Height="1" Background="{StaticResource BorderSubtle}" Margin="14,10" />
|
|
|
|
<TextBlock Classes="label" Text="SCOPES" Margin="14,0,14,8" />
|
|
|
|
<!--
|
|
Still not a selector. Every list on this screen now spans every vault this session holds a key
|
|
for, and each row names its own vault — so there is nothing to switch to. What the picker below
|
|
chooses is where a *new* item is filed, which is a different question and the only one that has
|
|
an answer worth asking for.
|
|
-->
|
|
<StackPanel Orientation="Horizontal" Margin="14,2" Spacing="7">
|
|
<Ellipse Width="6" Height="6" Fill="{StaticResource Accent}" VerticalAlignment="Center" />
|
|
<TextBlock Classes="mono" Text="{Binding HostsHeading}" FontSize="11"
|
|
Foreground="{StaticResource Text}" VerticalAlignment="Center" />
|
|
</StackPanel>
|
|
|
|
<!--
|
|
Hidden at one vault, which is where most people stay. A control offering a single option is a
|
|
question with no answer.
|
|
-->
|
|
<StackPanel Margin="14,10,14,0" Spacing="4" IsVisible="{Binding HasVaultChoice}">
|
|
<TextBlock Classes="label" Text="NEW ITEMS GO TO" />
|
|
<ComboBox ItemsSource="{Binding TargetVaults}"
|
|
SelectedItem="{Binding SelectedTargetVault}"
|
|
HorizontalAlignment="Stretch">
|
|
<ComboBox.ItemTemplate>
|
|
<DataTemplate x:DataType="vm:VaultChoiceViewModel">
|
|
<TextBlock Text="{Binding Display}" FontSize="12" />
|
|
</DataTemplate>
|
|
</ComboBox.ItemTemplate>
|
|
</ComboBox>
|
|
<TextBlock Classes="hint" FontSize="10.5" TextWrapping="Wrap"
|
|
Text="An item filed into a shared vault is readable by everyone holding that vault's key. It defaults to your own and never moves on its own. A host is asked separately, in its own editor." />
|
|
</StackPanel>
|
|
|
|
<!--
|
|
Items that would not decrypt. Shown here rather than only in the status line because this is the
|
|
screen the number is about, and because a non-zero count after a rekey is the signal that new
|
|
grants are needed rather than a transient.
|
|
-->
|
|
<Border Classes="chip warn" Margin="14,12,14,0" HorizontalAlignment="Left"
|
|
IsVisible="{Binding HasUnreadableItems}">
|
|
<TextBlock Text="{Binding UnreadableSummary}" />
|
|
</Border>
|
|
|
|
</StackPanel>
|
|
</ScrollViewer>
|
|
</Border>
|
|
|
|
<!-- The table -->
|
|
<Grid Grid.Column="1" RowDefinitions="Auto,Auto,*">
|
|
|
|
<Border Grid.Row="0" Padding="14,0" Height="44"
|
|
BorderBrush="{StaticResource Border}" BorderThickness="0,0,0,1">
|
|
<!--
|
|
◆ THE SUMMARY IS THE COLUMN THAT GIVES WAY, and that is what stops this header overflowing again.
|
|
|
|
It used to be Auto,Auto,*,Auto — a fixed title, a fixed summary, slack, then the buttons — so the
|
|
slack column was the only thing absorbing a change of width, and the strip fell off the right edge
|
|
the moment the five buttons wanted more than it had. That is not hypothetical: it is why GENERATE
|
|
lost the word KEY (see below), and it happened again the moment the type scale went up a point.
|
|
Buying pixels by shortening a caption fixes one instance of a shape that keeps producing them.
|
|
|
|
So the summary sits in the star column and trims, and the buttons are Auto and always get their
|
|
full width. That is the rule worth encoding rather than the pixels: a trimmed summary is a fact
|
|
you can read by widening the window, and a clipped button is a dead end. The titlebar's search box
|
|
is arranged this way for the same reason.
|
|
-->
|
|
<Grid ColumnDefinitions="Auto,*,Auto" VerticalAlignment="Center">
|
|
<TextBlock Grid.Column="0" Classes="mono" Text="{Binding SectionTitle}" FontSize="12"
|
|
FontWeight="SemiBold" LetterSpacing="1" Foreground="{StaticResource Text}"
|
|
VerticalAlignment="Center" />
|
|
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding SectionSummary}" FontSize="10.5"
|
|
Foreground="{StaticResource TextFaint}" Margin="10,0,10,0" VerticalAlignment="Center"
|
|
TextTrimming="CharacterEllipsis" />
|
|
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="6">
|
|
<!--
|
|
Always offered. Every category left on this screen is one things can be added to — the one
|
|
that was not, HOST KEYS, is now its own screen, and a pin still cannot be typed in there
|
|
either. See KnownHostsScreen.
|
|
|
|
GENERATE carries no KEY, which it lost when a fifth button arrived and the strip could still
|
|
overflow. The arrangement above is what keeps it inside now; the short caption stays because
|
|
its tooltip carries what the word did and the two key buttons are adjacent, so which one
|
|
generates is not in doubt.
|
|
-->
|
|
<Button Classes="ghost" Content="GENERATE" Command="{Binding NewGeneratedKeyCommand}"
|
|
ToolTip.Tip="Makes a new key pair here, so the private half never becomes a file on this disk." />
|
|
<Button Classes="ghost" Content="+ SSH KEY" Command="{Binding NewKeyCommand}"
|
|
ToolTip.Tip="Pastes in a key you already have." />
|
|
<Button Classes="ghost" Content="+ PASSWORD" Command="{Binding NewCredentialCommand}" />
|
|
<Button Classes="ghost" Content="+ TAG" Command="{Binding NewTagCommand}"
|
|
ToolTip.Tip="A name to put on hosts. Usually made from a host's editor instead; this is for setting a scheme up before there is anything to put it on." />
|
|
<Button Classes="accent" Content="+ BUCKET" Command="{Binding NewObjectStoreCommand}"
|
|
ToolTip.Tip="An S3-compatible bucket, to browse beside a host on the Files screen." />
|
|
</StackPanel>
|
|
</Grid>
|
|
</Border>
|
|
|
|
<!--
|
|
The design's columns are NAME / TYPE / FINGERPRINT / SCOPE / ACCESS / LAST. Three of those six have
|
|
nothing behind them: there is one scope, no roles, and no item carries a last-used or modified time —
|
|
VaultItem is (id, secret, version, three sync flags) and nothing else. What replaces them is the one
|
|
thing this build does know and the design had no column for: whether a change is still sitting in
|
|
this machine's outbox.
|
|
-->
|
|
<Grid Grid.Row="1" ColumnDefinitions="2,1.3*,74,*,88" Margin="0,6,14,6"
|
|
IsVisible="{Binding HasVaultItems}">
|
|
<TextBlock Grid.Column="1" Classes="label" Text="NAME" FontSize="9.5" LetterSpacing="1"
|
|
Margin="12,0,8,0" />
|
|
<TextBlock Grid.Column="2" Classes="label" Text="TYPE" FontSize="9.5" LetterSpacing="1" />
|
|
<TextBlock Grid.Column="3" Classes="label" Text="DETAIL" FontSize="9.5" LetterSpacing="1" />
|
|
<TextBlock Grid.Column="4" Classes="label" Text="STATE" FontSize="9.5" LetterSpacing="1" />
|
|
</Grid>
|
|
|
|
<ListBox Grid.Row="2" x:Name="ItemList" Focusable="True"
|
|
ItemsSource="{Binding VaultItems}"
|
|
SelectedItem="{Binding SelectedVaultItem}">
|
|
<ListBox.ItemTemplate>
|
|
<DataTemplate x:DataType="vm:VaultItemRowViewModel">
|
|
<Grid ColumnDefinitions="2,1.3*,74,*,88" Margin="0,7,14,7">
|
|
<Border Grid.Column="0" Classes="rowmark" />
|
|
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding Name}" FontSize="12"
|
|
FontWeight="Medium" Foreground="{StaticResource Text}" Margin="12,0,8,0"
|
|
TextTrimming="CharacterEllipsis" />
|
|
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Type}" FontSize="10"
|
|
Foreground="{StaticResource TextDim}" VerticalAlignment="Center" />
|
|
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding Detail}" FontSize="10.5"
|
|
Foreground="{StaticResource TextFaint}" Margin="0,0,8,0"
|
|
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
|
|
<Border Grid.Column="4" Classes="chip warn" HorizontalAlignment="Left"
|
|
VerticalAlignment="Center" IsVisible="{Binding HasBadge}">
|
|
<TextBlock Text="{Binding Badge}" FontSize="9.5" />
|
|
</Border>
|
|
</Grid>
|
|
</DataTemplate>
|
|
</ListBox.ItemTemplate>
|
|
</ListBox>
|
|
|
|
<!--
|
|
The empty state says which category is empty and what to do about it, rather than showing an empty
|
|
grid that reads as a list still loading.
|
|
-->
|
|
<TextBlock Grid.Row="2" Classes="hint" Text="{Binding EmptySectionMessage}" FontSize="12"
|
|
Margin="24" HorizontalAlignment="Center" VerticalAlignment="Center"
|
|
TextAlignment="Center" MaxWidth="320"
|
|
IsVisible="{Binding !HasVaultItems}" />
|
|
|
|
</Grid>
|
|
|
|
<!-- The detail pane, and the editors -->
|
|
<Border Grid.Column="2" Background="{StaticResource Sidebar}"
|
|
BorderBrush="{StaticResource Border}" BorderThickness="1,0,0,0">
|
|
<ScrollViewer>
|
|
<StackPanel Margin="14,16">
|
|
|
|
<!-- Nothing selected. -->
|
|
<TextBlock Classes="hint" FontSize="12"
|
|
Text="Choose something on the left to see what is known about it."
|
|
IsVisible="{Binding !HasSelectedVaultItem}" />
|
|
|
|
<StackPanel Spacing="6" IsVisible="{Binding HasSelectedVaultItem}">
|
|
<TextBlock Classes="mono" Text="{Binding SelectedVaultItem.Name}" FontSize="13"
|
|
FontWeight="SemiBold" Foreground="{StaticResource Text}" TextWrapping="Wrap" />
|
|
<StackPanel Orientation="Horizontal" Spacing="6">
|
|
<Border Classes="chip">
|
|
<TextBlock Text="{Binding SelectedVaultItem.Type}" />
|
|
</Border>
|
|
<Border Classes="chip accent">
|
|
<TextBlock Text="{Binding HostsHeading}" />
|
|
</Border>
|
|
</StackPanel>
|
|
|
|
<TextBlock Classes="label" Text="{Binding SelectedDetailHeading}" Margin="0,12,0,4" />
|
|
<Border Background="{StaticResource Raised}" BorderBrush="{StaticResource Border}"
|
|
BorderThickness="1" CornerRadius="4" Padding="8">
|
|
<SelectableTextBlock Classes="mono" Text="{Binding SelectedVaultItem.Detail}"
|
|
FontSize="10.5" Foreground="{StaticResource TextDim}"
|
|
TextWrapping="Wrap" />
|
|
</Border>
|
|
|
|
<!--
|
|
What the design puts here — who added it, when, who it is shared with, and a TEST CONNECT
|
|
button — has nothing behind it. Items carry no author, no timestamps and no sharing, and
|
|
nothing can exercise a credential without a host to exercise it against. Rather than five
|
|
empty rows, this says what is missing in one line.
|
|
-->
|
|
<TextBlock Classes="hint" FontSize="10.5" Margin="0,12,0,0"
|
|
Text="Keychain items record no author, no timestamps and no sharing yet, so there is nothing more to show here." />
|
|
|
|
<StackPanel Orientation="Horizontal" Spacing="6" Margin="0,14,0,0"
|
|
IsVisible="{Binding ShowsItemActions}">
|
|
<Button Classes="ghost" Content="EDIT" Command="{Binding EditSelectedItemCommand}" />
|
|
<!--
|
|
Only where there is somewhere to move to, unlike EDIT beside it, which is the same rule the
|
|
host's MOVE follows on the phone: a button that answers with "this is the only vault you can
|
|
write to" is a button that should not have been drawn. Keys and passwords only — a tag and a
|
|
bucket are read from the active vault alone, so "another vault" is not a question they have.
|
|
-->
|
|
<Button Classes="ghost" Content="MOVE" Command="{Binding MoveSelectedItemCommand}"
|
|
IsVisible="{Binding CanMoveSelectedItem}"
|
|
ToolTip.Tip="Re-encrypts this under another vault's key, and re-aims every host and group that used it at where it has gone." />
|
|
<Button Classes="danger" Content="DELETE" Command="{Binding DeleteSelectedItemCommand}" />
|
|
</StackPanel>
|
|
|
|
<!--
|
|
◆ MOVING THE ITEM TO ANOTHER VAULT, in the place those buttons were. The host's panel, over
|
|
here — see HostDrawer.axaml — and what it is for is the thing a shared vault could not do until
|
|
now: a key typed into a personal vault before the team existed was stuck there, and the only
|
|
way across was to paste the private half into a second item and delete the first.
|
|
|
|
The two sentences under the picker are the whole of the decision. The first says what a move
|
|
is; the second says what points at this key, because everything that does is re-aimed at it in
|
|
its new vault and somebody moving a key twenty machines use should see the twenty first.
|
|
-->
|
|
<StackPanel Spacing="8" Margin="0,14,0,0" IsVisible="{Binding IsMovingItem}">
|
|
<TextBlock Classes="label" Text="MOVE TO VAULT" />
|
|
<ComboBox HorizontalAlignment="Stretch" ItemsSource="{Binding MoveItemVaultChoices}"
|
|
SelectedItem="{Binding SelectedMoveItemVault}">
|
|
<ComboBox.ItemTemplate>
|
|
<DataTemplate x:DataType="vm:VaultChoiceViewModel">
|
|
<TextBlock Text="{Binding Display}" FontSize="12" />
|
|
</DataTemplate>
|
|
</ComboBox.ItemTemplate>
|
|
</ComboBox>
|
|
<TextBlock Classes="hint" FontSize="10.5" TextWrapping="Wrap"
|
|
Text="It is re-encrypted with the other vault's key, so everybody who holds that key can read it and nobody in the vault it leaves can." />
|
|
<TextBlock Classes="hint" FontSize="10.5" TextWrapping="Wrap"
|
|
IsVisible="{Binding HasMovingItemUsage}"
|
|
Text="{Binding MovingItemUsage}" />
|
|
<StackPanel Orientation="Horizontal" Spacing="6">
|
|
<Button Classes="accent" Content="MOVE" Command="{Binding ConfirmMoveItemCommand}"
|
|
IsEnabled="{Binding !IsBusy}" />
|
|
<Button Classes="ghost" Content="CANCEL" Command="{Binding CancelMoveItemCommand}" />
|
|
</StackPanel>
|
|
</StackPanel>
|
|
|
|
<!--
|
|
The public half only, and there is no button for the other one. Installing a key means pasting
|
|
this line into a host's authorized_keys; a private key on the clipboard is a private key in
|
|
every application on the machine.
|
|
-->
|
|
<Button Classes="ghost" Content="COPY PUBLIC KEY" Margin="0,6,0,0"
|
|
HorizontalAlignment="Left"
|
|
IsVisible="{Binding SelectedItemIsKey}"
|
|
Command="{Binding CopyPublicKeyCommand}"
|
|
ToolTip.Tip="Copies the authorized_keys line for this key, which is what a host needs to let it in." />
|
|
|
|
<!--
|
|
The question DELETE asks, in the place those two buttons were. Here rather than over the
|
|
screen, because this pane is where the item being deleted is described: the name, the kind and
|
|
what is stored are all still on screen above it, which is most of what somebody checks before
|
|
answering. See ConfirmDeleteCard.
|
|
-->
|
|
<Border Background="{StaticResource DangerWash}" BorderBrush="{StaticResource DangerSoft}"
|
|
BorderThickness="1" CornerRadius="4" Padding="10" Margin="0,14,0,0"
|
|
IsVisible="{Binding IsConfirmingDeletion}">
|
|
<views:ConfirmDeleteCard />
|
|
</Border>
|
|
|
|
</StackPanel>
|
|
|
|
<!--
|
|
Making a key, as opposed to pasting one in. A step of its own and a short one: an algorithm, a
|
|
comment, and a button. What it produces lands in the editor below, unsaved — so there is still
|
|
exactly one thing on this screen that writes a key, and it is still SAVE.
|
|
-->
|
|
<StackPanel Spacing="6" IsVisible="{Binding IsGeneratingKey}">
|
|
<TextBlock Classes="label" Text="NEW SSH KEY" Margin="0,0,0,4" />
|
|
|
|
<StackPanel Orientation="Horizontal" Spacing="6">
|
|
<!--
|
|
Buttons and a command rather than a selector bound to the algorithm, which is the same
|
|
choice the category rail makes and for the same reason: a selector moves its own highlight
|
|
before anything can refuse, so it can end up showing a choice nobody made.
|
|
-->
|
|
<Button Classes="flat choice" Content="ED25519"
|
|
Classes.active="{Binding GeneratesEd25519}"
|
|
Command="{Binding ChooseKeyAlgorithmCommand}"
|
|
CommandParameter="{x:Static ssh:SshKeyAlgorithm.Ed25519}"
|
|
ToolTip.Tip="What every current OpenSSH prefers. Small, fast, and generated instantly." />
|
|
<Button Classes="flat choice" Content="RSA 4096"
|
|
Classes.active="{Binding GeneratesRsa}"
|
|
Command="{Binding ChooseKeyAlgorithmCommand}"
|
|
CommandParameter="{x:Static ssh:SshKeyAlgorithm.Rsa4096}"
|
|
ToolTip.Tip="For servers too old to accept Ed25519. Larger, and a few seconds to generate." />
|
|
</StackPanel>
|
|
|
|
<TextBox Text="{Binding GenerateComment}" PlaceholderText="name — also the key's comment" />
|
|
<TextBlock Classes="hint" FontSize="10.5" TextWrapping="Wrap"
|
|
Text="This is what the key is called here and what is written into it, so the line on a host says where it came from." />
|
|
|
|
<!--
|
|
Said plainly rather than left to be discovered. Writing an encrypted openssh-key-v1 file needs
|
|
bcrypt_pbkdf, which .NET has no primitive for — and the defence it buys is one this product
|
|
already makes: a passphrase protects a key file on a disk, and this key is never on one.
|
|
-->
|
|
<TextBlock Classes="hint" FontSize="10.5" TextWrapping="Wrap" Margin="0,4,0,0"
|
|
Text="The key file itself has no passphrase. Your keychain passphrase is what protects it, and it never reaches the server in a form it can read." />
|
|
|
|
<StackPanel Orientation="Horizontal" Spacing="6" Margin="0,8,0,0">
|
|
<Button Classes="accent" Content="GENERATE" Command="{Binding GenerateKeyCommand}"
|
|
IsEnabled="{Binding !IsBusy}" />
|
|
<Button Classes="ghost" Content="CANCEL" Command="{Binding CancelGenerateKeyCommand}" />
|
|
</StackPanel>
|
|
</StackPanel>
|
|
|
|
<!-- The key editor. -->
|
|
<StackPanel Spacing="6" IsVisible="{Binding IsEditingKey}">
|
|
<TextBlock Classes="label" Text="SSH KEY" Margin="0,0,0,4" />
|
|
<TextBox Text="{Binding KeyEditorLabel}" PlaceholderText="name" />
|
|
<!--
|
|
Not a password box. The armour has to be visible to be pasted and checked — a masked
|
|
multi-line box makes "did the whole key arrive?" unanswerable — and the mistake this actually
|
|
prevents is pasting the .pub file, which SshKeySecret.TryValidate rejects by name.
|
|
-->
|
|
<TextBox Text="{Binding KeyEditorPrivateKey}"
|
|
PlaceholderText="-----BEGIN OPENSSH PRIVATE KEY-----"
|
|
AcceptsReturn="True" Height="96" TextWrapping="NoWrap"
|
|
FontFamily="{StaticResource MonoFont}" FontSize="11.5" />
|
|
<TextBox Text="{Binding KeyEditorPassphrase}"
|
|
PlaceholderText="passphrase, if the key has one" PasswordChar="•" />
|
|
<TextBox Text="{Binding KeyEditorPublicKey}" PlaceholderText="public half (optional)" />
|
|
<TextBox Text="{Binding KeyEditorNotes}" PlaceholderText="notes" AcceptsReturn="True"
|
|
Height="44" TextWrapping="Wrap" />
|
|
<TextBlock Classes="hint" FontSize="10.5"
|
|
Text="The key and its passphrase are encrypted here and never reach the server in a form it can read. Storing both together is the point of a keychain: on a disk the passphrase protects the key, and in here your keychain passphrase protects both." />
|
|
<StackPanel Orientation="Horizontal" Spacing="6">
|
|
<Button Classes="accent" Content="SAVE" Command="{Binding SaveKeyCommand}" />
|
|
<Button Classes="ghost" Content="CANCEL" Command="{Binding CancelKeyEditCommand}" />
|
|
</StackPanel>
|
|
</StackPanel>
|
|
|
|
<!-- The password editor. -->
|
|
<StackPanel Spacing="6" IsVisible="{Binding IsEditingCredential}">
|
|
<TextBlock Classes="label" Text="PASSWORD" Margin="0,0,0,4" />
|
|
<TextBox Text="{Binding CredentialEditorLabel}" PlaceholderText="name" />
|
|
<!--
|
|
Optional, and the reason a credential is worth being its own item rather than two more fields on
|
|
a host: one account on twenty machines is described once and rotated once. Left blank, each host
|
|
supplies its own username and only the password is shared.
|
|
-->
|
|
<TextBox Text="{Binding CredentialEditorUsername}"
|
|
PlaceholderText="username (blank: use each host's own)" />
|
|
<!--
|
|
Masked, unlike the private key box, and the difference is not inconsistency. A key's armour has
|
|
to be visible to be checked for truncation after a paste; a password is short, usually typed,
|
|
and shoulder-surfing is the likelier problem.
|
|
-->
|
|
<TextBox Text="{Binding CredentialEditorPassword}" PlaceholderText="password" PasswordChar="•" />
|
|
<TextBox Text="{Binding CredentialEditorNotes}" PlaceholderText="notes" AcceptsReturn="True"
|
|
Height="44" TextWrapping="Wrap" />
|
|
<TextBlock Classes="hint" FontSize="10.5"
|
|
Text="Encrypted here and never sent to the server in a form it can read. Bind a host to this in the host's own editor, under how it authenticates." />
|
|
<StackPanel Orientation="Horizontal" Spacing="6">
|
|
<Button Classes="accent" Content="SAVE" Command="{Binding SaveCredentialCommand}" />
|
|
<Button Classes="ghost" Content="CANCEL" Command="{Binding CancelCredentialEditCommand}" />
|
|
</StackPanel>
|
|
</StackPanel>
|
|
|
|
<!--
|
|
The tag editor, and the whole of it is one box. What it does not have is the point: renaming a
|
|
tag touches no host, because every host wearing it names its id. That is the entire reason a tag
|
|
is an item rather than a string repeated inside twenty payloads.
|
|
-->
|
|
<StackPanel Spacing="6" IsVisible="{Binding IsEditingTag}">
|
|
<TextBlock Classes="label" Text="TAG" Margin="0,0,0,4" />
|
|
<TextBox Text="{Binding TagEditorLabel}" PlaceholderText="name">
|
|
<TextBox.KeyBindings>
|
|
<KeyBinding Gesture="Enter" Command="{Binding SaveTagCommand}" />
|
|
</TextBox.KeyBindings>
|
|
</TextBox>
|
|
<TextBlock Classes="hint" FontSize="10.5"
|
|
Text="Renaming a tag changes it everywhere at once. No host is rewritten — each one names this tag rather than repeating its name." />
|
|
<StackPanel Orientation="Horizontal" Spacing="6">
|
|
<Button Classes="accent" Content="SAVE" Command="{Binding SaveTagCommand}" />
|
|
<Button Classes="ghost" Content="CANCEL" Command="{Binding CancelTagEditCommand}" />
|
|
</StackPanel>
|
|
</StackPanel>
|
|
|
|
<!-- The bucket editor. -->
|
|
<StackPanel Spacing="6" IsVisible="{Binding IsEditingObjectStore}">
|
|
<TextBlock Classes="label" Text="BUCKET" Margin="0,0,0,4" />
|
|
<TextBox Text="{Binding BucketEditorLabel}" PlaceholderText="name" />
|
|
<TextBox Text="{Binding BucketEditorBucket}" PlaceholderText="bucket" />
|
|
<TextBox Text="{Binding BucketEditorAccessKeyId}" PlaceholderText="access key id" />
|
|
<!--
|
|
Masked, like a password and for the same reason: a secret access key is one. The access key id
|
|
beside it is an identifier and is shown, which is also why the two are separate boxes.
|
|
-->
|
|
<TextBox Text="{Binding BucketEditorSecretAccessKey}" PlaceholderText="secret access key"
|
|
PasswordChar="•" />
|
|
<TextBox Text="{Binding BucketEditorRegion}" PlaceholderText="region (e.g. eu-west-1)" />
|
|
<!--
|
|
Blank means Amazon, and then the region resolves the host. Anything else is a full URL, which
|
|
is what makes this work against a self-hosted service.
|
|
-->
|
|
<TextBox Text="{Binding BucketEditorEndpoint}"
|
|
PlaceholderText="endpoint (blank: Amazon S3)" />
|
|
<CheckBox IsChecked="{Binding BucketEditorUsePathStyle}"
|
|
Content="Address the bucket as a path" />
|
|
<!--
|
|
Said where the decision is made. Getting this wrong produces a DNS failure whose message
|
|
mentions neither buckets nor this setting, which is the worst kind of thing to leave to a guess.
|
|
-->
|
|
<TextBlock Classes="hint" FontSize="10.5"
|
|
Text="Off for Amazon S3. On for most self-hosted services — MinIO and Ceph have no wildcard DNS, so the bucket cannot be a subdomain." />
|
|
<TextBox Text="{Binding BucketEditorNotes}" PlaceholderText="notes" AcceptsReturn="True"
|
|
Height="44" TextWrapping="Wrap" />
|
|
<TextBlock Classes="hint" FontSize="10.5"
|
|
Text="Encrypted here, keys and endpoint alike, and never sent to the server in a form it can read. Pick this bucket on the Files screen to browse it." />
|
|
<StackPanel Orientation="Horizontal" Spacing="6">
|
|
<Button Classes="accent" Content="SAVE" Command="{Binding SaveObjectStoreCommand}" />
|
|
<Button Classes="ghost" Content="CANCEL" Command="{Binding CancelObjectStoreEditCommand}" />
|
|
</StackPanel>
|
|
</StackPanel>
|
|
|
|
</StackPanel>
|
|
</ScrollViewer>
|
|
</Border>
|
|
|
|
</Grid>
|
|
|
|
</UserControl>
|