Public Access
Merge branch 'main' into the Android head
Main grew the screens the host-management plan called for — hosts, pins, snippets, logs, import, teams — plus the ObjectStore and Import projects behind two of them, and moved WindowsDeviceKeyStore into the desktop head's Platform folder. Five of those view models landed in a directory this branch had already moved, so they join the rest in DodoSSH.Client.Shell: git spotted the rename and put them there, and the namespaces followed. Shell picks up ObjectStore and Import as a result, which the Android head then gets transitively and will use neither of at first — scoped storage means there is no ~/.ssh/config to import, and file transfer is out of its first scope. Desktop suites green at 155 and 64.
This commit is contained in:
@@ -2,25 +2,30 @@
|
||||
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.VaultScreen"
|
||||
x:DataType="vm:VaultViewModel">
|
||||
|
||||
<!--
|
||||
Everything in the vault that is not a host: the keys, the stored passwords, and the host keys this user
|
||||
has approved.
|
||||
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.
|
||||
|
||||
Two of the design's five categories are not here. IDENTITIES and CERTIFICATES have no item type behind
|
||||
them — the vault holds exactly four kinds and two of those are hosts and pins — so listing them would be
|
||||
two headings that could never have anything under them. HOST KEYS is the other way round: a real,
|
||||
fully-backed category the design has no slot for. Both are recorded in docs/design-import-gaps.md.
|
||||
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.
|
||||
|
||||
The SCOPES rail below the categories is the vault list, which is real and today has one entry in it. The
|
||||
design shows three, two of them teams; team vaults exist as tables on the server and are refused by its
|
||||
access service, so a rail with three entries would be showing two vaults nothing can open.
|
||||
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">
|
||||
@@ -31,7 +36,7 @@
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="0,12">
|
||||
|
||||
<TextBlock Classes="label" Text="VAULT" Margin="14,0,14,8" />
|
||||
<TextBlock Classes="label" Text="KEYCHAIN" Margin="14,0,14,8" />
|
||||
|
||||
<Button Classes="flat cat" Command="{Binding ShowSectionCommand}"
|
||||
CommandParameter="{x:Static vm:VaultSection.All}"
|
||||
@@ -66,13 +71,18 @@
|
||||
</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.
|
||||
-->
|
||||
<Button Classes="flat cat" Command="{Binding ShowSectionCommand}"
|
||||
CommandParameter="{x:Static vm:VaultSection.KnownHosts}"
|
||||
Classes.active="{Binding ShowsKnownHosts}">
|
||||
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="HOST KEYS" Margin="12,0,0,0" />
|
||||
<TextBlock Grid.Column="2" Text="{Binding KnownHostPins.Count}"
|
||||
<TextBlock Grid.Column="1" Text="BUCKETS" Margin="12,0,0,0" />
|
||||
<TextBlock Grid.Column="2" Text="{Binding ObjectStores.Count}"
|
||||
Foreground="{StaticResource TextFaint}" />
|
||||
</Grid>
|
||||
</Button>
|
||||
@@ -82,18 +92,35 @@
|
||||
<TextBlock Classes="label" Text="SCOPES" Margin="14,0,14,8" />
|
||||
|
||||
<!--
|
||||
One entry per vault this session opened. Not a selector: every list on this screen reads the
|
||||
active vault, and a rail that let you click a vault you cannot switch to would be a control that
|
||||
does nothing. It is here because knowing which vault you are looking at is worth a line, and
|
||||
because this is where a second one appears when shared vaults arrive.
|
||||
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="10"
|
||||
Foreground="{StaticResource Text}" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
<TextBlock Classes="hint" FontSize="9.5" Margin="14,6,14,0"
|
||||
Text="One vault, because the server grants access to your own and refuses the rest. Sharing is a later milestone." />
|
||||
|
||||
<!--
|
||||
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="11" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
<TextBlock Classes="hint" FontSize="9.5" TextWrapping="Wrap"
|
||||
Text="An item filed into a team's vault is readable by everyone holding that vault's key. It defaults to your own and never moves on its own." />
|
||||
</StackPanel>
|
||||
|
||||
<!--
|
||||
Items that would not decrypt. Shown here rather than only in the status line because this is the
|
||||
@@ -121,10 +148,18 @@
|
||||
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding SectionSummary}" FontSize="9.5"
|
||||
Foreground="{StaticResource TextFaint}" Margin="10,0,0,0" VerticalAlignment="Center" />
|
||||
<StackPanel Grid.Column="3" 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.
|
||||
-->
|
||||
<Button Classes="ghost" Content="GENERATE KEY" 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}"
|
||||
IsVisible="{Binding CanAddToSection}" />
|
||||
<Button Classes="accent" Content="+ PASSWORD" Command="{Binding NewCredentialCommand}"
|
||||
IsVisible="{Binding CanAddToSection}" />
|
||||
ToolTip.Tip="Pastes in a key you already have." />
|
||||
<Button Classes="ghost" Content="+ PASSWORD" Command="{Binding NewCredentialCommand}" />
|
||||
<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>
|
||||
@@ -218,7 +253,7 @@
|
||||
empty rows, this says what is missing in one line.
|
||||
-->
|
||||
<TextBlock Classes="hint" FontSize="9.5" Margin="0,12,0,0"
|
||||
Text="Vault items record no author, no timestamps and no sharing yet, so there is nothing more to show here." />
|
||||
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}">
|
||||
@@ -226,6 +261,17 @@
|
||||
<Button Classes="danger" Content="DELETE" Command="{Binding DeleteSelectedItemCommand}" />
|
||||
</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
|
||||
@@ -238,19 +284,50 @@
|
||||
<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="9.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." />
|
||||
|
||||
<!--
|
||||
A pin has no editor and no Add, which is the one asymmetry on this screen and is deliberate:
|
||||
a pin appears because somebody approved a fingerprint at the moment of connecting, which is
|
||||
the one place it can be checked against what the operator published. What it does have is a
|
||||
way out, because a changed host key is refused outright and a rebuilt server would otherwise
|
||||
be unreachable for ever.
|
||||
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.
|
||||
-->
|
||||
<StackPanel Spacing="6" Margin="0,14,0,0" IsVisible="{Binding SelectedItemIsPin}">
|
||||
<TextBlock Classes="hint" FontSize="9.5"
|
||||
Text="Approved when you first connected. A pin outlives the host it was approved for, so one that says no host uses it is a leftover rather than a warning." />
|
||||
<Button Classes="danger" Content="FORGET THIS HOST KEY" HorizontalAlignment="Left"
|
||||
Command="{Binding ForgetPinCommand}"
|
||||
ToolTip.Tip="Withdraws every pinned key for this address, so the next connection asks you to check the fingerprint again. Takes effect immediately." />
|
||||
<TextBlock Classes="hint" FontSize="9.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>
|
||||
|
||||
@@ -273,7 +350,7 @@
|
||||
<TextBox Text="{Binding KeyEditorNotes}" PlaceholderText="notes" AcceptsReturn="True"
|
||||
Height="44" TextWrapping="Wrap" />
|
||||
<TextBlock Classes="hint" FontSize="9.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 vault: on a disk the passphrase protects the key, and in here your vault passphrase protects both." />
|
||||
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}" />
|
||||
@@ -307,6 +384,43 @@
|
||||
</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="9.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="9.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>
|
||||
|
||||
Reference in New Issue
Block a user