Skip to content
Open
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
6 changes: 6 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@
"npm: watch-tests"
],
"problemMatcher": []
},
{
// This task expects the arduino-ide repository to be checked out as a sibling folder of this repository.
"label": "Update VSIX in Arduino IDE",
"type": "shell",
"command": "rm -rf ../arduino-ide/electron-app/plugins/teensysecurity && mkdir -p ../arduino-ide/electron-app/plugins/teensysecurity && vsce package && unzip ./teensysecurity-0.0.1.vsix -d ../arduino-ide/electron-app/plugins/teensysecurity",
}
]
}
13 changes: 12 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,18 @@
"viewsWelcome": [
{
"view": "teensysecurity.setupView",
"contents": "1. Encryption Setup\n\nCreate a new encryption key. The key will be written at the path below. Keep this file secret. Anyone who obtains key.pem could decrypt your code. Make backups, as no way exists to recover this file.\n[Generate Key](command:teensysecurity.createKey)\n\n[Click](command:teensysecurity.showKeyPath) to show the location of the key.pem file.\n\nNormal code is stored in a \".HEX\" file an encrypted code is stored in an \".EHEX\" file. Both are created with every compile when key.pem exists at this path.\n\n2. Teensy Hardware Setup\n\nWrite your encryption key to Teensy's permanent fuse memory. After the key written, Teensy can run both normal and encrypted programs.\n[Fuse Write](command:teensysecurity.fuseWriteSketch)\n\nVerify an encrypted program runs properly.\n[Verify Sketch](command:teensysecurity.verifySketch)\n\nPermanently lock secure mode. Once locked, Teensy will only be able to run programs encrypted by your key, and JTAG access is disabled. This step is required for full security.\n[Lock Security](command:teensysecurity.lockSecuritySketch)"
"contents": "1. Encryption Setup\n\nCreate a new encryption key. The key will be written at the path below. Keep this file secret. Anyone who obtains key.pem could decrypt your code. Make backups, as no way exists to recover this file.\n[Generate Key](command:teensysecurity.createKey)",
"enablement": "teensysecurity.state == installed"
},
{
"view": "teensysecurity.setupView",
"contents": "[Click](command:teensysecurity.showKeyPath) to show the location of the key.pem file.\n\nNormal code is stored in a \".HEX\" file an encrypted code is stored in an \".EHEX\" file. Both are created with every compile when key.pem exists at this path.",
"enablement": "teensysecurity.state == installed && teensysecurity.hasKeyFile"
},
{
"view": "teensysecurity.setupView",
"contents": "2. Teensy Hardware Setup\n\nWrite your encryption key to Teensy's permanent fuse memory. After the key written, Teensy can run both normal and encrypted programs.\n[Fuse Write](command:teensysecurity.fuseWriteSketch)\n\nVerify an encrypted program runs properly.\n[Verify Sketch](command:teensysecurity.verifySketch)\n\nPermanently lock secure mode. Once locked, Teensy will only be able to run programs encrypted by your key, and JTAG access is disabled. This step is required for full security.\n[Lock Security](command:teensysecurity.lockSecuritySketch)",
"enablement": "teensysecurity.state == installed && teensysecurity.hasKeyFile"
}
]
},
Expand Down
226 changes: 151 additions & 75 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,53 +23,127 @@
// https://www.svgrepo.com/svg/34405/key


import * as vscode from 'vscode';
import type { Terminal, ExtensionTerminalOptions } from 'vscode';
import type { ArduinoContext } from 'vscode-arduino-api';
import * as path from 'path';
import * as cp from 'node:child_process';
import * as fs from 'node:fs';
import * as child_process from 'child_process';
import { tmpdir, platform } from 'node:os';
import { platform, tmpdir } from 'node:os';
import * as path from 'node:path';
import type { ExtensionTerminalOptions, Terminal } from 'vscode';
import * as vscode from 'vscode';
import type { ArduinoContext, BoardDetails } from 'vscode-arduino-api';
import { activateSetupView } from './setupView';


export function activate(context: vscode.ExtensionContext) {
// https://code.visualstudio.com/api/references/when-clause-contexts
function setWhenContext(contextKey: string, contextValue: unknown) {
return vscode.commands.executeCommand('setContext', `teensysecurity.${contextKey}`, contextValue);
}

// The VS Code API for Arduino IDE supports the FQBN (Fully Qualified Board Name) of the currently selected board in two different ways:
// - fqbn (string|undefined) -> This is when the user selects a board from the dialog (it does not mean the platform is installed).
// - boardDetails?.fqbn (string|undefined) -> This is when a board has been selected by the user and the IDE runs the `board details` command. When the platform is not installed, this value will be undefined.
// Currently, it is not possible to retrieve any information from the CLI via the extension APIs, so extensions cannot check whether a particular platform is installed.
// Extensions can work around this by listening to both the FQBN (when the user selects a board) and board detail (when the IDE resolves the selected board via the CLI) change events.
// If the FQBN changes and is "teensy:avr," the extension knows that the currently selected board is a Teensy.
// If the board details are undefined, the extension can deduce that the platform is not yet installed.
// This trick works only when the Teensy platform is installed via the "Boards Manager" and the platform name arch is `teensy:avr`. If the FQBN starts with a different vendor-arch pair, the string matching will not work.
// Such when context values should be provided by vscode-arduino-api as a feature and IDEs should implement it: https://github.com/dankeboy36/vscode-arduino-api/issues/17.

let availabilityState:
'selected' // when selected by user
| 'installed' // when selected by user + board details is available
| undefined // rest (loading, other board is selected, etc.)
= undefined;
let selectedBoardFqbn: string | undefined;

function activateWhenContext(arduinoContext: ArduinoContext): vscode.Disposable[] {
updateTeensySelectedWhenContext(arduinoContext.fqbn);
updateTeensyInstalledWhenContext(arduinoContext.boardDetails);
updateHasKeyFileWhenContext(arduinoContext.boardDetails);
return [
arduinoContext.onDidChange('fqbn')(updateTeensySelectedWhenContext),
arduinoContext.onDidChange('boardDetails')(updateTeensyInstalledWhenContext),
arduinoContext.onDidChange('boardDetails')(updateHasKeyFileWhenContext),
];
}

function updateTeensySelectedWhenContext(fqbn: string | undefined) {
selectedBoardFqbn = fqbn;
const isTeensy = selectedBoardFqbn?.startsWith('teensy:avr');
if (availabilityState === 'installed' && isTeensy) {
return;
}
availabilityState = isTeensy ? 'selected' : undefined;
return setWhenContext('state', availabilityState);
}

function updateTeensyInstalledWhenContext(details: BoardDetails | undefined) {
// board details change events always come after an FQBN change
if (availabilityState === 'selected' && details?.fqbn?.startsWith('teensy:avr')) {
availabilityState = 'installed';
return setWhenContext('state', availabilityState);
}
}

const acontext: ArduinoContext = vscode.extensions.getExtension(
function updateHasKeyFileWhenContext(boardDetails: BoardDetails | undefined) {
if (boardDetails?.fqbn.startsWith('teensy:avr')) {
const program = programPath(boardDetails, false);
if (program) {
const keyPath = keyFilename(program);
if (keyPath) {
setWhenContext('hasKeyFile', fs.existsSync(keyPath));
return;
}
}
}
setWhenContext('hasKeyFile', undefined);
}

export function activate(context: vscode.ExtensionContext) {
const arduinoContext: ArduinoContext = vscode.extensions.getExtension(
'dankeboy36.vscode-arduino-api'
)?.exports;
if (!acontext) {
if (!arduinoContext) {
console.log('teensysecurity Failed to load the Arduino API');
return;
}

activateSetupView(context);

context.subscriptions.push(
...activateWhenContext(arduinoContext),
vscode.commands.registerCommand('teensysecurity.createKey', () => {
var program = programpath(acontext);
if (!program) {return;}
var keyfile = keyfilename(program);
if (!keyfile) {return;}
var program = programPath(arduinoContext.boardDetails);
if (!program) { return; }
var keyfile = keyFilename(program);
if (!keyfile) { return; }
createKey(program, keyfile);
setWhenContext('hasKeyFile', true); // trigger a when context update after the command execution
})
);
context.subscriptions.push(
vscode.commands.registerCommand('teensysecurity.showKeyPath', () => {
var program = programpath(acontext);
if (!program) {return;}
var keyfile = keyfilename(program);
if (!keyfile) {return;}
vscode.window.showInformationMessage('key.pem location: ' + keyfile);
vscode.commands.registerCommand('teensysecurity.showKeyPath', async () => {
var program = programPath(arduinoContext.boardDetails);
if (!program) { return; }
var keyfile = keyFilename(program);
if (!keyfile) { return; }
const openKeyFileAction = 'Open Key File';
const actions = [];
if (fs.existsSync(keyfile)) {
actions.push(openKeyFileAction);
}
const action = await vscode.window.showInformationMessage('key.pem location: ' + keyfile, ...actions);
if (action === openKeyFileAction) {
vscode.commands.executeCommand('vscode.open', vscode.Uri.file(keyfile));
}
})
);
context.subscriptions.push(
vscode.commands.registerCommand('teensysecurity.fuseWriteSketch', async () => {
var program = programpath(acontext);
if (!program) {return;}
var keyfile = keyfilename(program);
if (!keyfile) {return;}
if (!keyfileexists(keyfile)) {return;}
var program = programPath(arduinoContext.boardDetails);
if (!program) { return; }
var keyfile = keyFilename(program);
if (!keyfile) { return; }
if (!keyfileexists(keyfile)) { return; }
console.log('teensysecurity.fuseWriteSketch (Fuse Write Sketch) callback');
var mydir = createTempFolder("FuseWrite");
makeCode(program, keyfile, "fuseino", path.join(mydir, "FuseWrite.ino"));
Expand All @@ -80,11 +154,11 @@ export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('teensysecurity.verifySketch', async () => {
console.log('teensysecurity.verifySketch (Verify Sketch) callback');
var program = programpath(acontext);
if (!program) {return;}
var keyfile = keyfilename(program);
if (!keyfile) {return;}
if (!keyfileexists(keyfile)) {return;}
var program = programPath(arduinoContext.boardDetails);
if (!program) { return; }
var keyfile = keyFilename(program);
if (!keyfile) { return; }
if (!keyfileexists(keyfile)) { return; }
var mydir = createTempFolder("VerifySecure");
makeCode(program, keyfile, "verifyino", path.join(mydir, "VerifySecure.ino"));
openSketch(mydir);
Expand All @@ -93,11 +167,11 @@ export function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(
vscode.commands.registerCommand('teensysecurity.lockSecuritySketch', async () => {
console.log('teensysecurity.lockSecuritySketch (Lock Security Sketch) callback');
var program = programpath(acontext);
if (!program) {return;}
var keyfile = keyfilename(program);
if (!keyfile) {return;}
if (!keyfileexists(keyfile)) {return;}
var program = programPath(arduinoContext.boardDetails);
if (!program) { return; }
var keyfile = keyFilename(program);
if (!keyfile) { return; }
if (!keyfileexists(keyfile)) { return; }
var mydir = createTempFolder("LockSecureMode");
makeCode(program, keyfile, "lockino", path.join(mydir, "LockSecureMode.ino"));
openSketch(mydir);
Expand All @@ -115,20 +189,20 @@ export function activate(context: vscode.ExtensionContext) {
console.log('extension "teensysecurity" is now active!');
}

function createTempFolder(sketchname: string) : string {
function createTempFolder(sketchname: string): string {
var mytmpdir = fs.mkdtempSync(path.join(tmpdir(), 'teensysecure-'));
var mydir:string = path.join(mytmpdir, sketchname);
var mydir: string = path.join(mytmpdir, sketchname);
console.log("temporary sketch directory: " + mydir);
fs.mkdirSync(mydir);
return mydir;
}

function makeCode(program: string, keyfile: string, operation: string, pathname: string) : boolean {
function makeCode(program: string, keyfile: string, operation: string, pathname: string): boolean {
// https://stackoverflow.com/questions/14332721
var child = child_process.spawnSync(program, [operation, keyfile]);
if (child.error) {return false;}
if (child.status != 0) {return false;}
if (child.stdout.length <= 0) {return false;}
var child = cp.spawnSync(program, [operation, keyfile]);
if (child.error) { return false; }
if (child.status !== 0) { return false; }
if (child.stdout.length <= 0) { return false; }
fs.writeFileSync(pathname, child.stdout);
return true;
}
Expand All @@ -137,16 +211,16 @@ async function openSketch(sketchpath: string) {
// Thanks to dankeboy36
// https://github.com/dankeboy36/vscode-arduino-api/discussions/16
const uri = vscode.Uri.file(sketchpath);
vscode.commands.executeCommand('vscode.openFolder', uri , { forceNewWindow: true });
vscode.commands.executeCommand('vscode.openFolder', uri, { forceNewWindow: true });
}

async function createKey(program: string, keyfile: string) {
// create a function to print to the terminal (EventEmitter so non-intuitive)
// https://code.visualstudio.com/api/references/vscode-api#EventEmitter&lt;T&gt;
var wevent = new vscode.EventEmitter<string>();
var isopen = false;
var buffer:string = '';
function tprint(s: string) : void {
var buffer: string = '';
function tprint(s: string): void {
var s2 = String(s).replace(/\n/g, "\r\n");
if (isopen) {
wevent.fire(s2);
Expand All @@ -157,25 +231,25 @@ async function createKey(program: string, keyfile: string) {

// open a terminal which will receive the keygen output messages
// https://code.visualstudio.com/api/references/vscode-api#ExtensionTerminalOptions
const opt : ExtensionTerminalOptions = {
const opt: ExtensionTerminalOptions = {
name: "New Key",
pty: {
onDidWrite: wevent.event,
open: () => { isopen = true; wevent.fire(buffer); buffer = ''; },
close: () => { isopen = false; buffer = ''; },
}
};
const term : Terminal = (<any>vscode.window).createTerminal(opt);
const term: Terminal = (<any>vscode.window).createTerminal(opt);
term.show();

// start teensy_secure running with keygen
var child = child_process.spawn(program, ['keygen', keyfile]);
var child = cp.spawn(program, ['keygen', keyfile]);

// as stdout and stderr arrive, send to the terminal
child.stdout.on('data', function(data:string) {
child.stdout.on('data', function (data: string) {
tprint(data);
});
child.stderr.on('data', function(data:string) {
child.stderr.on('data', function (data: string) {
tprint(data); // TODO: red text like esp-exception decoder
});

Expand All @@ -187,14 +261,16 @@ async function createKey(program: string, keyfile: string) {
// calling functions should NOT store this, only use if for immediate needs
// if Boards Manager is used to upgrade, downgrade or uninstall Teensy,
// this pathname can be expected to change or even become undefined
function programpath(acontext: ArduinoContext) : string | undefined {
var tool = findTool(acontext, "runtime.tools.teensy-tools");
function programPath(boardDetails: BoardDetails | undefined, showsErrorMessage = true): string | undefined {
var tool = findTool(boardDetails, "runtime.tools.teensy-tools");
if (!tool) {
vscode.window.showErrorMessage("Could not find teensy_secure utility. Please select a Teensy board from the drop-down list or Tools > Port menu.");
if (showsErrorMessage) {
vscode.window.showErrorMessage("Could not find teensy_secure utility. Please select a Teensy board from the drop-down list or Tools > Port menu.");
}
return undefined;
}
var filename = 'teensy_secure';
if (platform() === 'win32') {filename += '.exe';}
if (platform() === 'win32') { filename += '.exe'; }
return path.join(tool, filename);
}

Expand All @@ -203,43 +279,43 @@ function programpath(acontext: ArduinoContext) : string | undefined {
// teensy_secure can look for key.pem in multiple locations and choose which
// to use based on its internal rules. If the user moves or deletes their
// key.pem files, which file teensy_secure uses may change.
function keyfilename(program: string) : string | undefined {
function keyFilename(program: string): string | undefined {
// https://stackoverflow.com/questions/14332721
var child = child_process.spawnSync(program, ['keyfile']);
if (child.error) {return undefined;}
if (child.status != 0) {
var child = cp.spawnSync(program, ['keyfile']);
if (child.error) { return undefined; }
if (child.status !== 0) {
vscode.window.showErrorMessage("Found old version of teensy_secure utility. Please use Boards Manager to install Teensy 1.60.0 or later.");
return undefined;
}
if (child.stdout.length <= 0) {return undefined;}
var out:string = child.stdout.toString();
var out2:string = out.replace(/\s+$/gm,''); // remove trailing newline
if (child.stdout.length <= 0) { return undefined; }
var out: string = child.stdout.toString();
var out2: string = out.replace(/\s+$/gm, ''); // remove trailing newline
return out2;
}

function keyfileexists(keyfile: string) : boolean {
if (fs.existsSync(keyfile)) {return true;}
function keyfileexists(keyfile: string): boolean {
if (fs.existsSync(keyfile)) { return true; }
vscode.window.showErrorMessage('This command requires a key.pem file (' + keyfile + '). Please use "Teensy Security: Generate Key" to create your key.pem file.');
return false;
}

// from arduino-littlefs-upload
function findTool(ctx: ArduinoContext, match : string) : string | undefined {
var found = false;
var ret = undefined;
if (ctx.boardDetails !== undefined) {
Object.keys(ctx.boardDetails.buildProperties).forEach( (elem) => {
if (elem.startsWith(match) && !found && (ctx.boardDetails?.buildProperties[elem] !== undefined)) {
ret = ctx.boardDetails.buildProperties[elem];
found = true;
}
});
}
return ret;
function findTool(boardDetails: BoardDetails | undefined, match: string): string | undefined {
var found = false;
var ret = undefined;
if (boardDetails !== undefined) {
Object.keys(boardDetails.buildProperties).forEach((elem) => {
if (elem.startsWith(match) && !found && (boardDetails?.buildProperties[elem] !== undefined)) {
ret = boardDetails.buildProperties[elem];
found = true;
}
});
}
return ret;
}



// This method is called when your extension is deactivated
// TODO: should keep a list of all files create and delete them here
export function deactivate() {}
export function deactivate() { }