Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adjust project structure and simplify user messages. #87

Merged
merged 4 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 2 additions & 9 deletions l10n/bundle.l10n.zh-cn.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,10 @@
"Don't ask again": "不再询问",
"OK": "好的",
"Open setting": "打开设置",
", ": "、",
"Unable to retrieve any key in current folder.": "无法检索当前文件夹中的任何密钥。",
"Input the passphrase for the signing key": "输入密钥密码",
"Input the passphrase for the signing key, passphrase cache enabled": "输入密钥密码,密码安全存储功能已开启",
"For the key associated with {0}": "该密钥关联用户:{0}",
"Key unlocked, and the passphrase is stored in the SecretStorage of VSCode.": "密钥已解锁,密码已被安全存储在 VSCode 的机密存储区中。",
"The passphrase is stored in the SecretStorage of VSCode.": "密码已被安全存储在 VSCode 的机密存储区中。",
"Key unlocked.": "密钥已解锁。",
"Failed to unlock: {0}": "密钥解锁失败:{0}",
"There is no cached passphrase for your current key.": "您的当前密钥的密码并未被存储",
Expand All @@ -20,14 +18,9 @@
"Current key": "当前密钥",
"Rest keys": "其他密钥",
"List of keys with stored passphrases:": "已存储密码的密钥列表:",
"Do you really want to clear all your cached passphrase? This action CANNOT be reverted.": "您确认真的要删除您存储中的全部密码吗?该操作无法被撤销。",
"All Your cached passphrase has been cleared.": "您的全部密码已被从机密存储区中删除。",
"Key changed, and unlocked automatically using the previous-used stored passphrase.": "密钥已变更,并被通过机密存储区中的关联密码自动解锁。",
"Key re-locked, and unlocked automatically using the previous-used stored passphrase.": "密钥被自动锁定,现已被通过机密存储区中的关联密码自动解锁。",
"Key unlocked automatically using the previous-used stored passphrase.": "密钥已被通过机密存储区中的关联密码自动解锁。",
"Key changed, but the previous-used stored passphrase for this key is unable to unlock the key, so the passphrase has been deleted and you need to unlock the key manually.": "密钥已变更,但机密存储区中的关联密码无法解锁该密钥,故该密码已被删除,您需要手动解锁该密钥。",
"Key re-locked automatically, but the previous-used stored passphrase is unable to unlock the key, so the passphrase has been deleted and you need to unlock the key manually.": "密钥被自动锁定,但机密存储区中的关联密码无法解锁该密钥,故该密码已被删除,您需要手动解锁该密钥",
"The previous-used stored passphrase is unable to unlock current key, so the passphrase has been deleted and you need to unlock the key manually.": "机密存储区中的关联密码无法解锁当前密钥,故该密码已被删除,您需要手动解锁该密钥。",
"The previous-used stored passphrase is unable to unlock current key.": "机密存储区中的关联密码无法解锁当前密钥。",
"Key changed.": "密钥已变更。",
"Key re-locked.": "密钥被自动锁定。",
"No active folder": "当前无活动中的文件夹。",
Expand Down
2 changes: 1 addition & 1 deletion package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"gpgIndicator.l10n.commands.listPassphraseCache": "List your passphrase cache",
"gpgIndicator.l10n.configuration.statusRefreshInterval": "Background refresh interval (in seconds) for key status.",
"gpgIndicator.l10n.configuration.outputLogLevel": "Log level for extension log output.",
"gpgIndicator.l10n.configuration.enableSecurelyPassphraseCache": "Specifies whether to use the `SecretStorage` of VSCode to store your passphrase or not. [Read more](https://github.com/wdhongtw/vscode-gpg-indicator#passphrase-cache).\n\n**WARNING**: Turn off this option will clear all your passphrase cache, and cannot be reverted.",
"gpgIndicator.l10n.configuration.enableSecurelyPassphraseCache": "Specifies whether to use the `SecretStorage` of VSCode to store your passphrase or not. [Read more](https://github.com/wdhongtw/vscode-gpg-indicator#passphrase-cache).",
"gpgIndicator.l10n.configuration.statusStyle": "Specifies what to show about the current key in the status bar element",
"gpgIndicator.l10n.configuration.statusStyle.enumDescriptions.fingerprintWithUserId": "Show both fingerprint and user id (if any), if no user id found, show fingerprint only.\n\nExample: `0123456789ABCDEF - Example User <example@example.com>`",
"gpgIndicator.l10n.configuration.statusStyle.enumDescriptions.fingerprint": "Show fingerprint only.\n\nExample: `0123456789ABCDEF`",
Expand Down
2 changes: 1 addition & 1 deletion package.nls.zh-cn.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"gpgIndicator.l10n.commands.listPassphraseCache": "列举存有密码的密钥",
"gpgIndicator.l10n.configuration.statusRefreshInterval": "后台刷新间隔,以秒为单位。",
"gpgIndicator.l10n.configuration.outputLogLevel": "扩展日志输出等级,低于该等级的日志将不会被输出。",
"gpgIndicator.l10n.configuration.enableSecurelyPassphraseCache": "启用密钥密码的机密存储区存储功能,[查看详情](https://github.com/wdhongtw/vscode-gpg-indicator#passphrase-cache)。\n\n**警告**:禁用该功能会删除所有已存储的密钥,且无法撤销。",
"gpgIndicator.l10n.configuration.enableSecurelyPassphraseCache": "启用密钥密码的机密存储区存储功能,[查看详情](https://github.com/wdhongtw/vscode-gpg-indicator#passphrase-cache)。",
"gpgIndicator.l10n.configuration.statusStyle": "选择状态栏按钮显示的密钥信息内容。",
"gpgIndicator.l10n.configuration.statusStyle.enumDescriptions.fingerprintWithUserId": "同时显示密钥指纹和关联用户信息,若无关联用户信息则只显示密钥指纹。\n\n例如:`0123456789ABCDEF - Example User <example@example.com>`",
"gpgIndicator.l10n.configuration.statusStyle.enumDescriptions.fingerprint": "只显示密钥指纹。\n\n例如:`0123456789ABCDEF`",
Expand Down
File renamed without changes.
File renamed without changes.
2 changes: 1 addition & 1 deletion src/indicator/git.ts → src/adapter/git.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as util from 'util';

import * as core from '../manager';
import * as core from '../core';

const exec = util.promisify(require('child_process').exec);
// exec with default utf-8 encoding always return stdout as string
Expand Down
File renamed without changes.
6 changes: 3 additions & 3 deletions src/indicator/gpg.ts → src/adapter/gpg.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import * as process from './process';
import * as process from '../common/process';
import * as assuan from './assuan';

import type { Logger } from '../manager';
import * as core from '../manager';
import type { Logger } from '../core';
import * as core from '../core';

/**
* Get the path of socket file for communication with GPG agent.
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
68 changes: 42 additions & 26 deletions src/manager.ts → src/core.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import * as vscode from 'vscode';

import * as process from './indicator/process';
import { Mutex } from "./indicator/locker";
import { m } from "./message";
import { Mutex } from "./common/locker";

/**
* Logger is a sample interface for basic logging ability.
Expand Down Expand Up @@ -64,19 +60,13 @@ export default class KeyStatusManager {
private git: GitAdapter,
private gpg: GpgAdapter,
private secretStorage: Storage,
private receiver: EventReceiver,
public enablePassphraseCache: boolean,
private isWorkspaceTrusted: boolean,
private defaultFolder: string,
) {
}

private show(isChanged: boolean, changedMsg: string, defaultMsg: string) {
vscode.window.showInformationMessage(isChanged
? vscode.l10n.t(changedMsg)
: vscode.l10n.t(defaultMsg),
);
}

/** Trigger key status update once, coroutine-safe is ensured. */
async syncStatus(): Promise<void> {
await this.syncStatusLock.with(async () => {
Expand Down Expand Up @@ -144,22 +134,15 @@ export default class KeyStatusManager {

try {
await this.unlockCurrentKey(passphrase);
if (isUnlockedPrev) {
this.show(isChanged, m['keyChangedAndAutomaticallyUnlocked'], m['keyRelockedAndAutomaticallyUnlocked']);
} else {
this.show(isChanged, m['keyChangedAndAutomaticallyUnlocked'], m['keyAutomaticallyUnlocked']);
}
await this.receiver.onEvent(Event.StoredPassphraseUnlockSucceed);
} catch (err) {
if (!(err instanceof Error)) {
throw err;
}
this.logger.error(`Cannot unlock the key with the cached passphrase: ${err.message}`);
await this.secretStorage.delete(keyInfo.fingerprint);
if (isUnlockedPrev) {
this.show(isChanged, m['keyChangedButAutomaticallyUnlockFailed'], m['keyRelockedButAutomaticallyUnlockFailed']);
} else {
this.show(isChanged, m['keyChangedButAutomaticallyUnlockFailed'], m['keyAutomaticallyUnlockFailed']);
}
await this.receiver.onEvent(Event.StoredPassphraseBeDeleted);
await this.receiver.onEvent(Event.StoredPassphraseUnlockFailed);
}

return await this.gpg.isKeyUnlocked(keyInfo.keygrip);
Expand All @@ -173,8 +156,11 @@ export default class KeyStatusManager {
*/
private async showInfoOnly(isChanged: boolean, isUnlockedPrev: boolean, keyInfo: GpgKeyInfo): Promise<boolean> {
const isUnlocked = await this.gpg.isKeyUnlocked(keyInfo.keygrip);
if (isUnlockedPrev && !isUnlocked) {
this.show(isChanged, m['keyChanged'], m['keyRelocked']);

// We do not notify key changed event, since that it could be noisy potentially.
// For the same key, we only notify the change from "unlocked" to "locked".
if (!isChanged && isUnlockedPrev && !isUnlocked) {
await this.receiver.onEvent(Event.LockedStateEntered);
}

return isUnlocked;
Expand Down Expand Up @@ -259,11 +245,13 @@ export default class KeyStatusManager {
/** Lock or unlock current key */
async unlockCurrentKey(passphrase: string): Promise<void> {
if (this.activateFolder === undefined) {
throw new Error(vscode.l10n.t(m['noActiveFolder']));
this.logger.error("No activate folder");
return;
}

if (this.currentKey === undefined) {
throw new Error(vscode.l10n.t(m['noKeyForCurrentFolder']));
this.logger.error("No current key");
return;
}

if (await this.gpg.isKeyUnlocked(this.currentKey.keygrip)) {
Expand All @@ -274,6 +262,34 @@ export default class KeyStatusManager {
this.logger.info(`Try to unlock current key: ${this.currentKey.fingerprint}`);
await this.gpg.unlockByKey(this.currentKey.keygrip, passphrase);
}

/** Fetch all available GPG key information in user global scope. */
async getKeyInfos(): Promise<GpgKeyInfo[]> {
return await this.gpg.getKeyInfos();
}
}

/** Types for events from key manager. */
export enum Event {

/** Use stored passphrase to unlock and succeed. */
StoredPassphraseUnlockSucceed,

/** Use stored passphrase to unlock buf failed. */
StoredPassphraseUnlockFailed,

/** Previously stored passphrase be deleted. */
StoredPassphraseBeDeleted,

/** Some key changed into locked state. */
LockedStateEntered,
};

/** Receiver interface for events from key manager. */
export interface EventReceiver {

/** Handle given event. */
onEvent(event: Event): Promise<void>;
}

/** The abstract storage for our application, focusing on string type. */
Expand Down
3 changes: 2 additions & 1 deletion src/extension.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import * as vscode from 'vscode';
import * as assert from 'assert';

import * as core from './core';

import * as extension from './extension';
import * as core from './manager';

class FakeCipher implements extension.Cipher {
constructor() { }
Expand Down
64 changes: 41 additions & 23 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import * as os from 'os';
import * as crypto from 'crypto';
import * as util from 'util';

import * as git from './indicator/git';
import * as gpg from './indicator/gpg';
import * as locker from './indicator/locker';
import { Logger } from "./manager";
import KeyStatusManager from "./manager";
import { Storage, KeyStatusEvent } from "./manager";
import * as git from './adapter/git';
import * as gpg from './adapter/gpg';
import * as locker from './common/locker';
import * as core from './core';
import { Logger } from "./core";
import KeyStatusManager from "./core";
import { Storage, KeyStatusEvent } from "./core";
import { m } from "./message";

type statusStyleEnum = "fingerprintWithUserId" | "fingerprint" | "userId";
Expand Down Expand Up @@ -36,7 +37,7 @@ async function generateKeyList(secretStorage: PassphraseStorage, keyStatusManage
return false;
}
const items: vscode.QuickPickItem[] = [];
const keyInfos = await gpg.getKeyInfos();
const keyInfos = await keyStatusManager.getKeyInfos();
const keyToUser = keyInfos.map(({ userId, fingerprint }): [string, string?] => [fingerprint, userId]);
const withUsers = keyToUser.filter((pair): pair is [string, string] => pair[1] !== undefined);
const keyList = new Map<string, string>(withUsers);
Expand Down Expand Up @@ -77,6 +78,27 @@ async function generateKeyList(secretStorage: PassphraseStorage, keyStatusManage
return items;
}

/** MessageEventReceiver transform event into VSCode information message. */
class MessageEventReceiver implements core.EventReceiver {

async onEvent(event: core.Event): Promise<void> {
const message: string = (() => {
switch (event) {
case core.Event.StoredPassphraseUnlockSucceed:
return vscode.l10n.t(m['keyAutomaticallyUnlocked']);
case core.Event.StoredPassphraseUnlockFailed:
return vscode.l10n.t(m['keyAutomaticallyUnlockFailed']);
case core.Event.StoredPassphraseBeDeleted:
return vscode.l10n.t(m['passphraseDeleted']);
case core.Event.LockedStateEntered:
return vscode.l10n.t(m['keyRelocked']);
}
})();

await vscode.window.showInformationMessage(message);
}
}

export async function activate(context: vscode.ExtensionContext) {
const masterKey = await initializeMasterKey(context.secrets);

Expand All @@ -99,6 +121,7 @@ export async function activate(context: vscode.ExtensionContext) {
new git.CliGit(),
new gpg.CliGpg(logger),
secretStorage,
new MessageEventReceiver(),
configuration.get<boolean>('enablePassphraseCache', false),
vscode.workspace.isTrusted,
os.homedir(),
Expand All @@ -118,9 +141,7 @@ export async function activate(context: vscode.ExtensionContext) {
return;
}
const passphrase = await vscode.window.showInputBox({
prompt: keyStatusManager.enablePassphraseCache
? vscode.l10n.t(m['passphraseInputPromptTitleWhenSecurelyPassphraseCacheEnabled'])
: vscode.l10n.t(m['passphraseInputPromptTitle']),
prompt: vscode.l10n.t(m['passphraseInputPromptTitle']),
password: true,
placeHolder: currentKey.userId
? vscode.l10n.t(m['keyDescriptionWithUserId'], currentKey.userId)
Expand All @@ -138,11 +159,10 @@ export async function activate(context: vscode.ExtensionContext) {

if (keyStatusManager.enablePassphraseCache) {
await secretStorage.set(currentKey.fingerprint, passphrase);
vscode.window.showInformationMessage(vscode.l10n.t(m['keyUnlockedWithCachedPassphrase']));
} else {
vscode.window.showInformationMessage(vscode.l10n.t(m['keyUnlocked']));
await introduceCacheFeature(context);
vscode.window.showInformationMessage(vscode.l10n.t(m['passphraseStored']));
}
vscode.window.showInformationMessage(vscode.l10n.t(m['keyUnlocked']));
await introduceCacheFeature(context);
}));
keyStatusItem.tooltip = 'Unlock this key';
keyStatusItem.command = commandId;
Expand Down Expand Up @@ -186,14 +206,9 @@ export async function activate(context: vscode.ExtensionContext) {
vscode.window.showInformationMessage(vscode.l10n.t(m['noCachedPassphrase']));
return;
}
if ((await vscode.window.showInformationMessage<vscode.MessageItem>(
vscode.l10n.t(m["passphraseClearanceConfirm"]),
{ modal: true },
{ title: actions.YES },
{ title: actions.NO, isCloseAffordance: true },
))?.title !== actions.YES) {
return;
}

// We do not confirm again whether user really want to delete, just trust our user.

await Promise.all([...secretStorage].map((key) => secretStorage.delete(key)));
vscode.window.showInformationMessage(vscode.l10n.t(m['passphraseCleared']));
}));
Expand Down Expand Up @@ -299,6 +314,9 @@ export async function deactivate() {

async function introduceCacheFeature(context: vscode.ExtensionContext) {
const configuration = vscode.workspace.getConfiguration('gpgIndicator');
if (configuration.get<boolean>('enablePassphraseCache', false)) {
return;
}

if (await context.globalState.get("user:is-cache-notice-read")) {
return;
Expand All @@ -315,7 +333,7 @@ async function introduceCacheFeature(context: vscode.ExtensionContext) {
}
await context.globalState.update("user:is-cache-notice-read", true);
if (result === actions.YES) {
configuration.update("enablePassphraseCache", true, true);
await configuration.update("enablePassphraseCache", true, true);
}

let postMessage: string;
Expand Down
11 changes: 2 additions & 9 deletions src/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,10 @@ export const m = {
"actionDoNotAskAgain": "Don't ask again",
"actionOK": "OK",
"actionOpenSetting": "Open setting",
"separator": ", ",
"noKeyInCurrentFolder": "Unable to retrieve any key in current folder.",
"passphraseInputPromptTitle": "Input the passphrase for the signing key",
"passphraseInputPromptTitleWhenSecurelyPassphraseCacheEnabled": "Input the passphrase for the signing key, passphrase cache enabled",
"keyDescriptionWithUserId": "For the key associated with {0}",
"keyUnlockedWithCachedPassphrase": "Key unlocked, and the passphrase is stored in the SecretStorage of VSCode.",
"passphraseStored": "The passphrase is stored in the SecretStorage of VSCode.",
"keyUnlocked": "Key unlocked.",
"keyUnlockFailedWithId": "Failed to unlock: {0}",
"noCachedPassphraseForCurrentKey": "There is no cached passphrase for your current key.",
Expand All @@ -20,14 +18,9 @@ export const m = {
"currentKey": "Current key",
"restKey": "Rest keys",
"cachedPassphraseList": "List of keys with stored passphrases:",
"passphraseClearanceConfirm": "Do you really want to clear all your cached passphrase? This action CANNOT be reverted.",
"passphraseCleared": "All Your cached passphrase has been cleared.",
"keyChangedAndAutomaticallyUnlocked": "Key changed, and unlocked automatically using the previous-used stored passphrase.",
"keyRelockedAndAutomaticallyUnlocked": "Key re-locked, and unlocked automatically using the previous-used stored passphrase.",
"keyAutomaticallyUnlocked": "Key unlocked automatically using the previous-used stored passphrase.",
"keyChangedButAutomaticallyUnlockFailed": "Key changed, but the previous-used stored passphrase for this key is unable to unlock the key, so the passphrase has been deleted and you need to unlock the key manually.",
"keyRelockedButAutomaticallyUnlockFailed": "Key re-locked automatically, but the previous-used stored passphrase is unable to unlock the key, so the passphrase has been deleted and you need to unlock the key manually.",
"keyAutomaticallyUnlockFailed": "The previous-used stored passphrase is unable to unlock current key, so the passphrase has been deleted and you need to unlock the key manually.",
"keyAutomaticallyUnlockFailed": "The previous-used stored passphrase is unable to unlock current key.",
"keyChanged": "Key changed.",
"keyRelocked": "Key re-locked.",
"noActiveFolder": "No active folder",
Expand Down