Skip to content
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
23 changes: 23 additions & 0 deletions docs/API-Reference/utils/NodeUtils.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,26 @@ This validates that the system-wide license file exists, contains valid JSON, an

**Kind**: global function
**Returns**: <code>Promise.&lt;boolean&gt;</code> - - Resolves with `true` if the device is licensed, `false` otherwise.
<a name="getOSUserName"></a>

## getOSUserName() ⇒ <code>Promise.&lt;string&gt;</code>
Retrieves the operating system username of the current user.
This method is only available in native apps.

**Kind**: global function
**Returns**: <code>Promise.&lt;string&gt;</code> - A promise that resolves to the OS username of the current user.
**Throws**:

- <code>Error</code> Throws an error if called in a browser environment.

<a name="getSystemSettingsDir"></a>

## getSystemSettingsDir() ⇒ <code>Promise.&lt;string&gt;</code>
Retrieves the directory path for system settings. This method is applicable to native apps only.

**Kind**: global function
**Returns**: <code>Promise.&lt;string&gt;</code> - A promise that resolves to the path of the system settings directory.
**Throws**:

- <code>Error</code> If the method is called in browser app.

16 changes: 16 additions & 0 deletions src-node/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const os = require('os');

exports.SYSTEM_SETTINGS_DIR_WIN = 'C:\\Program Files\\Phoenix Code Control\\';
exports.SYSTEM_SETTINGS_DIR_MAC = '/Library/Application Support/phoenix-code-control/';
exports.SYSTEM_SETTINGS_DIR_LINUX = '/etc/phoenix-code-control/';

switch (os.platform()) {
case 'win32':
exports.SYSTEM_SETTINGS_DIR = exports.SYSTEM_SETTINGS_DIR_WIN; break;
case 'darwin':
exports.SYSTEM_SETTINGS_DIR = exports.SYSTEM_SETTINGS_DIR_MAC; break;
case 'linux':
exports.SYSTEM_SETTINGS_DIR = exports.SYSTEM_SETTINGS_DIR_LINUX; break;
default:
throw new Error(`Unsupported platform: ${os.platform()}`);
}
176 changes: 176 additions & 0 deletions src-node/licence-device.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
const os = require('os');
const sudo = require('@expo/sudo-prompt');
const fs = require('fs');
const fsPromise = require('fs').promises;
const path = require('path');
const { exec } = require('child_process');
const { SYSTEM_SETTINGS_DIR } = require('./constants');

const options = { name: 'Phoenix Code' };
const licenseFileContent = JSON.stringify({});

function getLicensePath() {
return `${SYSTEM_SETTINGS_DIR}device-license`;
}

function sudoExec(command) {
return new Promise((resolve, reject) => {
sudo.exec(command, options, (error, stdout, stderr) => {
if (error) {
return reject(error);
}
resolve({ stdout, stderr });
});
});
}

function readFileUtf8(p) {
return new Promise((resolve, reject) => {
fs.readFile(p, 'utf8', (err, data) => (err ? reject(err) : resolve(data)));
});
}

/**
* Writes the license file in a world-readable location.
* Works on Windows, macOS, and Linux.
*/
async function addDeviceLicense() {
const targetPath = getLicensePath();
let command;
// we should not store any sensitive information in this file as this is world readable. we use the
// device id itself as license key for that machine. the device id is not associated with any cloud credits
// and all entitlements are local to device only for this threat model to work. So stolen device IDs doesn't
// have any meaning.

if (os.platform() === 'win32') {
// Windows: write file and explicitly grant Everyone read rights
const dir = 'C:\\Program Files\\Phoenix Code Control';
command =
`powershell -Command "` +
`New-Item -ItemType Directory -Force '${dir}' | Out-Null; ` +
`Set-Content -Path '${targetPath}' -Value '${licenseFileContent}' -Encoding UTF8; ` +
`icacls '${targetPath}' /inheritance:e /grant *S-1-1-0:RX | Out-Null"`;
} else {
// macOS / Linux: mkdir + write + chmod 0644 (world-readable, owner-writable)
const dir = path.dirname(targetPath);
command =
`/bin/mkdir -p "${dir}"` +
` && printf '%s' '${licenseFileContent}' > "${targetPath}"` +
` && /bin/chmod 0644 "${targetPath}"`;
}

await sudoExec(command);
return targetPath;
}

async function removeDeviceLicense() {
const targetPath = getLicensePath();
let command;

if (os.platform() === 'win32') {
command = `powershell -Command "if (Test-Path '${targetPath}') { Remove-Item -Path '${targetPath}' -Force }"`;
} else {
command = `/bin/rm -f "${targetPath}"`;
}

await sudoExec(command);
return targetPath;
}

async function isLicensedDevice() {
const targetPath = getLicensePath();
try {
const data = await readFileUtf8(targetPath);
JSON.parse(data.trim());
return true; // currently, the existence of the file itself is flag. in future, we may choose to add more.
} catch {
// file missing, unreadable, or invalid JSON
return false;
}
}

async function _getLinuxDeviceID() {
const data = await fsPromise.readFile("/etc/machine-id", "utf8");
const id = data.trim();
return id || null;
// throw on error to main.
// no fallback, /var/lib/dbus/machine-id may need sudo in some machines
}

/**
* Get the macOS device ID (IOPlatformUUID).
* @returns {Promise<string|null>}
*/
function _getMacDeviceID() {
// to read this in mac bash, do:
// #!/bin/bash
// device_id=$(ioreg -rd1 -c IOPlatformExpertDevice | awk -F\" '/IOPlatformUUID/ {print $4}' | tr -d '[:space:]')
// echo "$device_id"
return new Promise((resolve, reject) => {
exec(
'ioreg -rd1 -c IOPlatformExpertDevice | grep IOPlatformUUID',
{ encoding: 'utf8' },
(err, stdout) => {
if (err) {
console.error('Failed to get Mac device ID:', err.message);
return reject(err);
}

const match = stdout.match(/"IOPlatformUUID" = "([^"]+)"/);
if (match && match[1]) {
resolve(match[1]);
} else {
resolve(null);
}
}
);
});
}

/**
* Get the Windows device ID (MachineGuid).
* @returns {Promise<string|null>}
*
* In a Windows batch file, you can get this with:
* reg query HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography /v MachineGuid
*/
function _getWindowsDeviceID() {
return new Promise((resolve, reject) => {
exec(
'reg query HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography /v MachineGuid',
{ encoding: 'utf8' },
(err, stdout) => {
if (err) {
console.error('Failed to get Windows device ID:', err.message);
return reject(err);
}

// Example output:
// HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography
// MachineGuid REG_SZ 4c4c4544-0034-5a10-8051-cac04f305a31
const match = stdout.match(/MachineGuid\s+REG_[A-Z]+\s+([a-fA-F0-9-]+)/);
if (match && match[1]) {
resolve(match[1].trim());
} else {
resolve(null);
}
}
);
});
}

async function getDeviceID() {
if (process.platform === "linux") {
return _getLinuxDeviceID();
} else if (process.platform === "darwin") {
return _getMacDeviceID();
} else if (process.platform === "win32") {
return _getWindowsDeviceID();
}
throw new Error(`Unsupported platform: ${process.platform}`);
}

exports.addDeviceLicense = addDeviceLicense;
exports.removeDeviceLicense = removeDeviceLicense;
exports.isLicensedDevice = isLicensedDevice;
exports.getDeviceID = getDeviceID;
Loading
Loading