@asrient/create-homecloud-creds 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +217 -0
- package/package.json +13 -0
package/index.js
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Interactive script to generate credentials for the HomeCloud Server.
|
|
5
|
+
*
|
|
6
|
+
* Generates RSA key pair, links the device to an account via the auth server,
|
|
7
|
+
* encrypts the private key, and outputs the credentials file.
|
|
8
|
+
*
|
|
9
|
+
* Usage: node tools/create-server-creds.js
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const crypto = require('node:crypto');
|
|
13
|
+
const readline = require('node:readline');
|
|
14
|
+
|
|
15
|
+
const SERVER_URL = 'https://homecloudapi.asrient.com';
|
|
16
|
+
|
|
17
|
+
const rl = readline.createInterface({
|
|
18
|
+
input: process.stdin,
|
|
19
|
+
output: process.stdout
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function ask(question) {
|
|
23
|
+
return new Promise((resolve) => {
|
|
24
|
+
rl.question(question, resolve);
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function generateKeyPair() {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
crypto.generateKeyPair('rsa', { modulusLength: 2048 }, (err, publicKey, privateKey) => {
|
|
31
|
+
if (err) return reject(err);
|
|
32
|
+
resolve({
|
|
33
|
+
privateKeyPem: privateKey.export({ type: 'pkcs8', format: 'pem' }).toString(),
|
|
34
|
+
publicKeyPem: publicKey.export({ type: 'pkcs1', format: 'pem' }).toString(),
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getKeyFromPem(pem) {
|
|
41
|
+
const match = pem.match(/-----BEGIN (.*)-----([^-]*)-----END \1-----/);
|
|
42
|
+
if (!match) throw new Error('Invalid PEM format');
|
|
43
|
+
return match[2].replace(/\n/g, '');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getFingerprint(publicKeyPem) {
|
|
47
|
+
const base64Key = getKeyFromPem(publicKeyPem);
|
|
48
|
+
return crypto.createHash('sha256').update(base64Key).digest('hex');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function sign(data, privateKeyPem) {
|
|
52
|
+
const signer = crypto.createSign('RSA-SHA512');
|
|
53
|
+
signer.update(data);
|
|
54
|
+
signer.end();
|
|
55
|
+
return signer.sign(privateKeyPem);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function deriveKey(passphrase, salt) {
|
|
59
|
+
return crypto.scryptSync(passphrase, salt, 32);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function encryptString(text, key) {
|
|
63
|
+
const iv = crypto.randomBytes(16);
|
|
64
|
+
const cipher = crypto.createCipheriv('aes-256-ctr', key, iv);
|
|
65
|
+
const encrypted = Buffer.concat([cipher.update(text, 'utf8'), cipher.final()]);
|
|
66
|
+
return {
|
|
67
|
+
iv: iv.toString('hex'),
|
|
68
|
+
payload: encrypted.toString('hex'),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function createSignedPacket(data, privateKeyPem, publicKeyPem) {
|
|
73
|
+
const dataStr = JSON.stringify(data);
|
|
74
|
+
const signature = sign(Buffer.from(dataStr), privateKeyPem);
|
|
75
|
+
return {
|
|
76
|
+
data: dataStr,
|
|
77
|
+
signature: signature.toString('base64'),
|
|
78
|
+
publicKeyPem,
|
|
79
|
+
expireAt: Date.now() + 3 * 60 * 1000,
|
|
80
|
+
nonce: crypto.randomUUID(),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function initiateLink(serverUrl, packet) {
|
|
85
|
+
const resp = await fetch(`${serverUrl}/api/link`, {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
headers: { 'Content-Type': 'application/json' },
|
|
88
|
+
body: JSON.stringify(packet),
|
|
89
|
+
});
|
|
90
|
+
if (!resp.ok) {
|
|
91
|
+
const body = await resp.text();
|
|
92
|
+
throw new Error(`Link initiation failed (${resp.status}): ${body}`);
|
|
93
|
+
}
|
|
94
|
+
return resp.json();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function verifyLink(serverUrl, requestId, pin) {
|
|
98
|
+
const resp = await fetch(`${serverUrl}/api/link-verify`, {
|
|
99
|
+
method: 'POST',
|
|
100
|
+
headers: { 'Content-Type': 'application/json' },
|
|
101
|
+
body: JSON.stringify({ requestId, pin }),
|
|
102
|
+
});
|
|
103
|
+
if (!resp.ok) {
|
|
104
|
+
const body = await resp.text();
|
|
105
|
+
throw new Error(`Link verification failed (${resp.status}): ${body}`);
|
|
106
|
+
}
|
|
107
|
+
return resp.json();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function main() {
|
|
111
|
+
console.log('=== HomeCloud Server Credentials Generator ===\n');
|
|
112
|
+
|
|
113
|
+
const email = await ask('Email: ');
|
|
114
|
+
if (!email.trim()) {
|
|
115
|
+
console.error('Email is required.');
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const serverName = await ask('Server name: ');
|
|
120
|
+
if (!serverName.trim()) {
|
|
121
|
+
console.error('Server name is required.');
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
console.log('\nGenerating RSA key pair...');
|
|
126
|
+
const { privateKeyPem, publicKeyPem } = await generateKeyPair();
|
|
127
|
+
const fingerprint = getFingerprint(publicKeyPem);
|
|
128
|
+
console.log(`Fingerprint: ${fingerprint}`);
|
|
129
|
+
|
|
130
|
+
const peerInfo = {
|
|
131
|
+
deviceName: serverName.trim(),
|
|
132
|
+
fingerprint,
|
|
133
|
+
version: '0.0.1',
|
|
134
|
+
deviceInfo: {
|
|
135
|
+
os: 'linux',
|
|
136
|
+
osFlavour: null,
|
|
137
|
+
formFactor: 'server',
|
|
138
|
+
},
|
|
139
|
+
iconKey: null,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
console.log('\nInitiating account link...');
|
|
143
|
+
const packet = await createSignedPacket({
|
|
144
|
+
email: email.trim(),
|
|
145
|
+
accountId: null,
|
|
146
|
+
fingerprint,
|
|
147
|
+
peerInfo,
|
|
148
|
+
}, privateKeyPem, publicKeyPem);
|
|
149
|
+
|
|
150
|
+
const linkResp = await initiateLink(SERVER_URL, packet);
|
|
151
|
+
console.log(`Request ID: ${linkResp.requestId}`);
|
|
152
|
+
|
|
153
|
+
let verifyResp;
|
|
154
|
+
if (linkResp.requiresVerification) {
|
|
155
|
+
console.log(`\nA verification PIN has been sent to ${email.trim()}`);
|
|
156
|
+
const pin = await ask('Enter PIN: ');
|
|
157
|
+
if (!pin.trim()) {
|
|
158
|
+
console.error('PIN is required.');
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
verifyResp = await verifyLink(SERVER_URL, linkResp.requestId, pin.trim());
|
|
162
|
+
} else {
|
|
163
|
+
verifyResp = await verifyLink(SERVER_URL, linkResp.requestId, null);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
console.log(`\nAccount linked successfully!`);
|
|
167
|
+
console.log(`Account ID: ${verifyResp.accountId}`);
|
|
168
|
+
|
|
169
|
+
const passphrase = await ask('\nChoose a passphrase (min 6 chars): ');
|
|
170
|
+
if (!passphrase.trim() || passphrase.trim().length < 6) {
|
|
171
|
+
console.error('Passphrase must be at least 6 characters.');
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const salt = crypto.randomBytes(16);
|
|
176
|
+
const derivedKey = deriveKey(passphrase.trim(), salt);
|
|
177
|
+
const encrytPrivatePem = encryptString(privateKeyPem, derivedKey);
|
|
178
|
+
|
|
179
|
+
const creds = {
|
|
180
|
+
publicPem: publicKeyPem,
|
|
181
|
+
encrytPrivatePem,
|
|
182
|
+
salt: salt.toString('hex'),
|
|
183
|
+
accountId: verifyResp.accountId,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const credsJson = JSON.stringify(creds, null, 2);
|
|
187
|
+
|
|
188
|
+
console.log('\n=== Passphrase (save this — required to start the server) ===');
|
|
189
|
+
console.log(passphrase.trim());
|
|
190
|
+
|
|
191
|
+
console.log('\n--- Output ---');
|
|
192
|
+
const fs = require('node:fs');
|
|
193
|
+
const path = require('node:path');
|
|
194
|
+
const resolved = path.resolve('./creds.json');
|
|
195
|
+
fs.writeFileSync(resolved, credsJson, 'utf-8');
|
|
196
|
+
console.log(`Credentials written to ${resolved}`);
|
|
197
|
+
console.log(`\nSet these env vars to run the server:`);
|
|
198
|
+
console.log(` PASSPHRASE=${passphrase.trim()}`);
|
|
199
|
+
console.log(` CREDS_PATH=path/to/creds.json`);
|
|
200
|
+
|
|
201
|
+
const wantBase64 = await ask('\nAlso copy as base64? [y/N]: ');
|
|
202
|
+
if (wantBase64.trim().toLowerCase() === 'y') {
|
|
203
|
+
const base64 = Buffer.from(credsJson).toString('base64');
|
|
204
|
+
console.log('\n=== Base64 Credentials (copy this) ===');
|
|
205
|
+
console.log(base64);
|
|
206
|
+
console.log(`\nUse this instead of CREDS_PATH:`);
|
|
207
|
+
console.log(` CREDS_BASE64=<the base64 string above>`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
rl.close();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
main().catch((err) => {
|
|
214
|
+
console.error(`\nError: ${err.message}`);
|
|
215
|
+
rl.close();
|
|
216
|
+
process.exit(1);
|
|
217
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@asrient/create-homecloud-creds",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Generate credentials for HomeCloud Server",
|
|
5
|
+
"bin": "index.js",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Asrient",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/asrient/HomeCloud.git",
|
|
11
|
+
"directory": "serverCreds"
|
|
12
|
+
}
|
|
13
|
+
}
|