照烟照烟
笔记

密码学环境配置

安装 WSL

什么是 WSL

WSL 是 Windows Subsystem for Linux 的缩写,即“适用于 Linux 的 Windows 子系统”。它让你能在 Windows 上直接运行 Linux 命令行、工具和程序,无需安装虚拟机或双系统。WSL2 使用真实 Linux 内核,性能和兼容性更好。

为什么要安装 WSL

主要还是因为 sagemath 撤销了对 Windows 的支持。另外以后如果跑 agent 的话,在 Windows 环境下 powershell 命令容易被写坏,agent 对 Linux 命令的支持度更高。再者,想学 Linux 命令、测试部署环境,或使用 Docker、Git 等工具时,WSL 是很方便的选择。

使用命令行安装

在安装之前,先查看可用的发行版:

1
wsl --list --online

版本有很多。这里为了适配主流 Linux 操作命令,我们安装最新版的 Ubuntu-26.04:

1
wsl --install -d Ubuntu-26.04

安装成功后,会提示让我们创建默认账户。这个账户是 普通的非管理员用户账户,但具备执行 sudo 的权限。

1
2
3
4
5
6
7
正在下载: Ubuntu 26.04 LTS
正在安装: Ubuntu 26.04 LTS
已成功安装分发。可以通过 “wsl.exe -d Ubuntu-26.04” 启动它
正在启动 Ubuntu-26.04...
Provisioning the new WSL instance Ubuntu-26.04
This might take a while...
Create a default Unix user account:

用户名按自己喜欢的来就好,但首字母不能大写。之后如果改名字可能会导致一堆莫名其妙的报错,所以尽量现在想好用户名,以后不要改动。

之后会提示输入密码。因为个人电脑没什么安全风险,所以可以先设一个弱密码,等会再配置使用 sudo 命令时无需输入密码,达到类似无密码的效果。

后面的匿名系统数据收集计划输入 y 同意就好,至此我们就成功进入 Ubuntu 系统,还是非常简单的。

Linux 基础配置

之后我们就可以通过 powershell 的扩展框进入 Linux 终端:

初次进入,可能会有点不习惯终端的配色风格和光标。这些可以在上图扩展框的设置中调整,进入后如下图所示。可以更换图标,也能把光标从方块更改为细线式,更改终端背景颜色(或把背景换为图片),字体大小及样式等。我个人喜欢 Campbell 的配色方案,Cascadia Code 字体,竖线光标。当然,因人而异嘛。

配置好后,前缀大概类似 cheyanne@LAPTOP-GRNTPI58:~$

如果觉得这个主机名看着很别扭,可以自己更改。先编辑第一份配置文件:

1
sudo nano /etc/wsl.conf

在 nano 中写入以下内容(wslname 换成自己喜欢的名字)

1
2
3
[network]
hostname = wslname
generateHosts = false

写入后按顺序按 ctrl+Oenterctrl+X 保存并退出。接着编辑第二份配置文件:

1
sudo nano /etc/hosts

把其中带有你的主机名的部分全部替换为你自己起的主机名,同样按 ctrl+Oenterctrl+X 保存并退出。

改完后在 Windows powershell 中 执行 wsl --shutdown,再次进入终端就修改成功了。

类似 cheyanne@ubuntu:~$

需要知道的是,每个 Linux 用户都被分配了一个家目录(用 ~ 表示,根目录用 / 表示)。平常写脚本改代码时尽量在家目录里活动,不要到根目录里随便改文件,容易改崩。等到安装的程序多了,家目录容易变得很乱。我个人推荐在家目录下新建一个文件夹,平常在那里敲代码。文件夹名字假设为 main,新建文件夹:

1
mkdir ~/main

设置启动终端时默认目录为 ~/main(而非原本的直接进入 ~):

编辑 ~/.bashrc

1
nano ~/.bashrc

末尾新增一行:

1
cd ~/main

保存并退出后使更改生效:

1
source ~/.bashrc

这样以后进入终端的默认目录就是 ~/main 了。

需要的话,配置 sudo 命令无需密码:

1
2
sudo su
visudo

在末尾加上这段(username 换成自己的):

1
username ALL=(ALL:ALL) NOPASSWD: ALL

对于 Ubuntu-26.06,使用以下命令更换下载源(其它版本不要用这个):

1
sudo sed -i 's|http://archive.ubuntu.com/ubuntu|https://mirrors.aliyun.com/ubuntu|g; s|http://security.ubuntu.com/ubuntu|https://mirrors.aliyun.com/ubuntu|g' /etc/apt/sources.list.d/ubuntu.sources

再更新软件源(换源后下载速度应该快很多):

1
2
3
sudo apt update
sudo apt upgrade -y
sudo apt install -y build-essential git curl wget unzip zip ca-certificates gdb file patchelf

还有一些基础的 Linux 命令(比如 cdls)和快捷键(比如 ctrl+L 清空终端,ctrl+A 将光标移动到输入框开头等)需要大家自己学习,这里就不再赘述了。

安装 conda 及 sagemath

什么是 conda

Conda 是一个开源的包管理和环境管理工具,主要用于安装和管理软件包及其依赖。它能自动处理依赖关系,让安装、更新和卸载更加方便,同时还可以为不同项目创建独立的运行环境,有效避免库版本冲突。

安装 conda

安装过程中一路 yes 或者回车就好,这里安装的是 Miniforge3:

1
2
wget https://mirrors.ustc.edu.cn/github-release/conda-forge/miniforge/LatestRelease/Miniforge3-Linux-x86_64.sh
bash Miniforge3-Linux-x86_64.sh

完成之后执行:

1
2
rm Miniforge3-Linux-x86_64.sh
source ~/.bashrc

安装成功之后前缀会有一个 (base) 标记。配置:

1
2
3
conda config --set channel_priority strict
conda config --set solver libmamba
conda config --show channels

安装 sagemath

mamba 创建 sage 环境:

1
mamba create -y -n sage sage

验证能否进入 sage 环境:

1
conda activate sage

如果能看到 (sage) 前缀,那么恭喜你,最难的任务已经完成了 🥰

真实使用时,我们不需要激活环境,直接指名道姓使用就可以。先退出 sage 环境:

1
conda deactivate

如果退到了 base 环境,再执行一次上面的命令就行。

关闭自动启动 base:

1
conda config --set auto_activate_base false

测试能否通过绝对路径使用 sagemath:

1
~/miniforge3/envs/sage/bin/sage

界面是这样的:

可以测试一些简单的命令,验证能不能正常使用:

1
2
3
4
print("Hello, Hnusec!")
1 + 1
factor(91) # 91的质因数分解
quit() # 退出交互

每次都得输绝对路径太麻烦了,我们在配置中设置 sage 命令对路径 ~/miniforge3/envs/sage/bin/sage 的映射(虽然一般不直接在这个交互界面使用 sage):

1
2
3
4
mkdir -p ~/.local/bin
ln -s ~/miniforge3/envs/sage/bin/sage ~/.local/bin/sage
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc

这样以后直接输入 sage 就可以进交互终端,不用输绝对路径了。

安装第三方模块

如果一切正常,现在 Ubuntu 里应该有三个 python 环境:系统 python、conda-base 的 python,以及 sagemath 内置的 python。

切记不要在终端里直接 pip install ...,此时操作的是系统 python,会报错如下。不要动系统 python。

conda-base 的 python 虽然可以用,但是没什么必要(毕竟写一般的 python 脚本都在 Win 里,且 sage 的 python 完全可以承担它的功能),所以我们安装第三方模块都是在 sage 的 python 里。

使用以下命令在 sage 的 python 安装第三方模块:

1
~/miniforge3/bin/mamba run -n sage pip install 包名

下面的命令一般不使用。优先用上面的 pip 安装:

1
~/miniforge3/bin/mamba install -n sage 包名

举个例子。在没安装 pycryptodome 库之前,直接导入会报错:

使用命令安装:

1
~/miniforge3/bin/mamba run -n sage pip install pycryptodome

之后就可以正常使用库函数:

配置 VSCode 及其扩展

首先确保你的 Win 系统中已经有 VSCode。在 Win 的 VSCode 中下载 WSL 扩展:

安装好后在 Ubuntu 终端中输入:

1
code .

后面的 . 代表把当前目录设为工作区,当然也可以打开别的目录。

除了在 Ubuntu 终端中打开 VSCode,也可以在 Win 的 VSCode 中打开 Ubuntu 的 VSCode,如图:

如果你的 VSCode 设置了窗口缩放,启动 WSL 后缩放就没有了,可以在 Win 中 VSCode 的 settings.json 末尾新增 "window.zoomLevel": 1, 以锁定窗口缩放。

对于密码方向来说,WSL 的 VSCode 主要是用来写 python 和 sage 的,扩展不用装太多。推荐这些:

  • Python 全家桶(Python、Pylance、Black Formatter 等等)
  • SageMath Enhanced(对于 sage 脚本生效,简单的语法高亮补全和一键运行)
  • Jupyter(虽然我没怎么用过)

安装好扩展后可以按 ctrl+shift+P 重加载窗口。关于 VSCode 的快捷键也有非常多,用好了会很方便。

再次按 ctrl+shift+P 选择 Python 解释器为 sage,如图:

语法差异

如果题目刚需 sage 环境(比如涉及到 coppersmith、ECC 以及格密码时),只依靠交互环境是很费劲的,这个时候就需要编写 sage 脚本解题。但是目前 .sage 没有成熟的代码补全和高亮。虽然 python 能导入 sage 库(from sage.all import *),能够享受 lsp,但是语法却有许多限制。对比一下 sage 写法和 python 写法的差异:

  1. 对于精确有理数(如 13 ):sage 写 a = 1/3 能精确保留,python 需要额外写成 a = QQ(1)/3

  2. 幂运算:sage 用 ^(异或是 ^^),python 用 **(异或是 ^

  3. 单变量多项式环:R.<x> = QQ[],python中取生成元是x = QQ["x"].gen(),取环是R = QQ["x"]。二者都要的话要写成两句:R = QQ["x"]; x = R.gen()

    类比所有带 .<x> 形式的语法,python 都不能直接用(如多变量多项式环,商环,数域等等)

  4. 连续步长的列表:[1..5],这个语法糖的意味太重了,python 是 list(range(1, 5+1))

大概就只有这么多,其它的 sage 语法在 python 编辑器里不会报错,可以照常写。

直接写 .sage 脚本可以享受更便捷的语法,但是失去了高亮和补全。写 .py (这里指调用 sage 库函数的脚本)正好相反,语法复杂一些,但是体验更好。

修改扩展(可选)

SageMath Enhanced 扩展有些地方用着不是很顺手:

  1. 当你试图按住 ctrl 以查看类 / 方法的定义时(其实更可能是按快捷键时鼠标不小心移到了上面),扩展会报错 Request textDocument/definition failed.,且这个报错不能静默。
  2. 写矩阵时如果分行写会报警告(原因是原扩展只维护了同一行的括号栈),Unmatched parentheses on line x
  3. 点击右上角的 Run SageMath File 小箭头后文件不会自动保存,只按照旧脚本运行。
  4. (可选的优化)保存时若代码末尾无空行,则新增一个空行并将光标移动到该空行上。

接下来的扩展修复均基于目录 ~/.vscode-server/extensions/lov3.sagemath-enhanced-2.0.8,一定在修复之前备份原扩展。

每次修改后记得使用 ctrl+shift+P 重载窗口以使修改生效。

语言服务器请求错误

文件:

1
out/server/src/server.js

out/server/src/server.js:113,服务端声明支持 textDocument/definition,但文件中没有注册 connection.onDefinition(...)。Ctrl+悬停触发定义请求时,服务端返回“方法未实现”,于是 VSCode 显示 Request textDocument/definition failed.

直接删除该能力声明。这样 VSCode 不再向此语言服务器发送定义请求,错误消失。

多行矩阵误报 Unmatched parentheses on line x

文件:

1
out/server/src/server.js

修改前

documents.onDidChangeContent(...) 后面直接进入 validateTextDocument,并在 validateTextDocument 的循环里逐行数括号。原逻辑大致在 validateTextDocument 内:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const diagnostics = [];
// Basic syntax checking for SageMath
const lines = text.split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Check for common SageMath syntax issues
// Check for unmatched parentheses, brackets, braces
const openParens = (line.match(/\(/g) || []).length;
const closeParens = (line.match(/\)/g) || []).length;
if (openParens !== closeParens) {
const diagnostic = {
severity: node_1.DiagnosticSeverity.Warning,
range: {
start: { line: i, character: 0 },
end: { line: i, character: line.length }
},
message: `Unmatched parentheses on line ${i + 1}`,
source: 'sagemath-enhanced'
};
diagnostics.push(diagnostic);
}
// Check for undefined variables that might be typos

修改后

documents.onDidChangeContent(...)async function validateTextDocument(...) 之间新增以下函数和常量。位置约为 out/server/src/server.js:187

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
const OPENING_DELIMITERS = {
'(': ')',
'[': ']',
'{': '}'
};
const CLOSING_DELIMITERS = {
')': '(',
']': '[',
'}': '{'
};
function createDelimiterDiagnostic(line, character, lineLength, message) {
return {
severity: node_1.DiagnosticSeverity.Warning,
range: {
start: { line, character },
end: { line, character: Math.min(character + 1, Math.max(lineLength, character + 1)) }
},
message,
source: 'sagemath-enhanced'
};
}
function collectDelimiterDiagnostics(lines) {
const diagnostics = [];
const stack = [];
let stringState = undefined;
for (let lineNumber = 0; lineNumber < lines.length; lineNumber++) {
const line = lines[lineNumber];
for (let character = 0; character < line.length; character++) {
const char = line[character];
if (stringState) {
if (stringState.escaped) {
stringState.escaped = false;
continue;
}
if (char === '\\') {
stringState.escaped = true;
continue;
}
if (stringState.triple) {
if (line.startsWith(stringState.quote.repeat(3), character)) {
character += 2;
stringState = undefined;
}
}
else if (char === stringState.quote) {
stringState = undefined;
}
continue;
}
if (char === '#') {
break;
}
if (char === '"' || char === "'") {
const triple = line.startsWith(char.repeat(3), character);
stringState = { quote: char, triple, escaped: false };
if (triple) {
character += 2;
}
continue;
}
if (OPENING_DELIMITERS[char]) {
stack.push({ char, line: lineNumber, character });
continue;
}
if (CLOSING_DELIMITERS[char]) {
const opener = stack[stack.length - 1];
if (opener && opener.char === CLOSING_DELIMITERS[char]) {
stack.pop();
continue;
}
diagnostics.push(createDelimiterDiagnostic(lineNumber, character, line.length, `Unmatched closing delimiter '${char}' on line ${lineNumber + 1}`));
}
}
if (stringState && !stringState.triple) {
stringState = undefined;
}
}
for (const opener of stack) {
diagnostics.push(createDelimiterDiagnostic(opener.line, opener.character, lines[opener.line].length, `Unmatched opening delimiter '${opener.char}' on line ${opener.line + 1}; expected '${OPENING_DELIMITERS[opener.char]}'`));
}
return diagnostics;
}

然后把 validateTextDocument 开头的括号诊断逻辑改成调用新函数。位置约为 out/server/src/server.js:274

1
2
3
4
5
6
7
const diagnostics = [];
// Basic syntax checking for SageMath
const lines = text.split('\n');
diagnostics.push(...collectDelimiterDiagnostics(lines));
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Check for undefined variables that might be typos

也就是删除原来的逐行 openParens/closeParens 统计块。

点击 Run SageMath File 前自动保存

文件:

1
out/src/extension.js

修改前

当前命令回调原来是同步函数,且校验 Sage 文件后直接读取磁盘路径运行。原逻辑大致在 out/src/extension.js:34

1
2
3
4
5
6
7
8
9
10
11
12
const runDisposable = vscode.commands.registerCommand('runsagemathfile.run', () => {
const editor = vscode.window.activeTextEditor;
if (!editor) {
console.log('No editor is active');
return;
}
const document = editor.document;
if (document.languageId !== 'sage') {
vscode.window.showInformationMessage('The active file is not a SageMath file');
return;
}
const sagePath = vscode.workspace.getConfiguration().get('sagemathEnhanced.interpreterPath');

修改后

把回调改成 async,并在读取 sagePath 前保存脏文档。修改后位置约为 out/src/extension.js:63

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const runDisposable = vscode.commands.registerCommand('runsagemathfile.run', async () => {
const editor = vscode.window.activeTextEditor;
if (!editor) {
console.log('No editor is active');
return;
}
const document = editor.document;
if (document.languageId !== 'sage') {
vscode.window.showInformationMessage('The active file is not a SageMath file');
return;
}
if (document.isDirty) {
const saved = await document.save();
if (!saved) {
vscode.window.showErrorMessage('SageMath file was not saved. Run cancelled.');
return;
}
}
const sagePath = vscode.workspace.getConfiguration().get('sagemathEnhanced.interpreterPath');

后续构造终端命令的代码保持不变。

保存时补末尾空行并移动光标

文件:

1
out/src/extension.js

修改前

function activate(context) { 后直接注册运行命令:

1
2
3
function activate(context) {
// Register the run command first to ensure it's available even if language server fails
const runDisposable = vscode.commands.registerCommand('runsagemathfile.run', async () => {

并且订阅列表原来只有运行命令和重启命令:

1
context.subscriptions.push(runDisposable, restartDisposable);

修改后

function activate(context) { 后新增保存监听。修改后位置约为 out/src/extension.js:33

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function activate(context) {
const moveCursorAfterSave = new Set();
const ensureTrailingBlankLineDisposable = vscode.workspace.onWillSaveTextDocument(event => {
const document = event.document;
if (document.languageId !== 'sage') {
return;
}
const text = document.getText();
if (!text || text.endsWith('\n') || text.endsWith('\r')) {
return;
}
const lastLine = document.lineAt(document.lineCount - 1);
const endPosition = new vscode.Position(document.lineCount - 1, lastLine.text.length);
const eol = document.eol === vscode.EndOfLine.CRLF ? '\r\n' : '\n';
moveCursorAfterSave.add(document.uri.toString());
event.waitUntil(Promise.resolve([vscode.TextEdit.insert(endPosition, eol)]));
});
const moveCursorToTrailingBlankLineDisposable = vscode.workspace.onDidSaveTextDocument(document => {
const uri = document.uri.toString();
if (!moveCursorAfterSave.delete(uri)) {
return;
}
const editor = vscode.window.activeTextEditor;
if (!editor || editor.document.uri.toString() !== uri) {
return;
}
const position = new vscode.Position(document.lineCount - 1, 0);
editor.selection = new vscode.Selection(position, position);
editor.revealRange(new vscode.Range(position, position));
});
// Register the run command first to ensure it's available even if language server fails

并把订阅列表改为 out/src/extension.js:130 的形式:

1
context.subscriptions.push(ensureTrailingBlankLineDisposable, moveCursorToTrailingBlankLineDisposable, runDisposable, restartDisposable);