Skip to content

Commit ab0899b

Browse files
committed
initial
1 parent 672e55e commit ab0899b

5 files changed

Lines changed: 391 additions & 51 deletions

File tree

dist/index.html

Lines changed: 0 additions & 1 deletion
This file was deleted.

dist/solid-ui.min.js.LICENSE.txt

Lines changed: 0 additions & 50 deletions
This file was deleted.

src/chat/crypto.ts

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
import { literal, quad, Statement } from 'rdflib';
2+
// import { PODCHAT, removeHashFromUrl } from './Constants';
3+
import { authn } from 'solid-logic'
4+
import * as UI from 'solid-ui'
5+
import * as $rdf from 'rdflib'
6+
7+
const ns = UI.ns
8+
9+
const removeHashFromUrl = (url: string) => {
10+
const newUrl = new URL(url)
11+
newUrl.hash = ''
12+
return newUrl.href
13+
}
14+
const PODCHAT_NS = 'https://www.pod-chat.com/';
15+
const PODCHAT = {
16+
LongChatMessage: PODCHAT_NS + 'LongChatMessage',
17+
LongChatMessageReply: PODCHAT_NS + 'LongChatMessageReply',
18+
RSAPublicKey: PODCHAT_NS + 'RSAPublicKey',
19+
RSAPrivateKey: PODCHAT_NS + 'RSAPrivateKey',
20+
signature: PODCHAT_NS + 'signature'
21+
}
22+
23+
// import rdfStore, { extractObjectLastValue } from './RdfStore';
24+
25+
export type RdfStore = {
26+
cache: Store,
27+
fetcher: Fetcher,
28+
updateManager: UpdateManager
29+
}
30+
function extractObject({ cache }: RdfStore, webid: string, resourceUrl: string, predicate: PredicateType): Array<Node> {
31+
return cache.each(cache.sym(webid), predicate, undefined, cache.sym(resourceUrl));
32+
}
33+
function extractObjectLastValue(rdfStore: RdfStore, webid: string, resourceUrl: string, predicate: PredicateType): string | undefined {
34+
return extractObject(rdfStore, webid, resourceUrl, predicate).map(q => q.value).pop();
35+
}
36+
37+
38+
39+
export const prepareRsaKeyPair = async (profileId: string, rsaPrivateKeyResourceUrl: string): Promise<void> => {
40+
41+
const privKey = await getPrivateKey(rsaPrivateKeyResourceUrl);
42+
const pubKey = await getPublicKey(profileId);
43+
44+
if (!privKey || !pubKey) {
45+
await createKeyPair(profileId, rsaPrivateKeyResourceUrl);
46+
const privKeyNew = await getPrivateKey(rsaPrivateKeyResourceUrl);
47+
const pubKeyNew = await getPublicKey(profileId);
48+
if (!privKeyNew || !pubKeyNew) {
49+
throw new Error('Unable to create RSA keypair.');
50+
}
51+
}
52+
}
53+
54+
export const signMessage = async (rsaPrivateKeyResourceUrl: string, messageContent: string): Promise<string | undefined> => {
55+
const privateKey = await getPrivateKey(rsaPrivateKeyResourceUrl);
56+
if (privateKey) {
57+
const messageContentEnc = new TextEncoder().encode(messageContent);
58+
const signature = await window.crypto.subtle.sign(
59+
{
60+
name: "RSA-PSS",
61+
saltLength: 32,
62+
},
63+
privateKey,
64+
messageContentEnc
65+
);
66+
const exportedAsString = ab2str(signature);
67+
const exportedAsBase64 = window.btoa(exportedAsString);
68+
return exportedAsBase64;
69+
}
70+
71+
return undefined;
72+
}
73+
74+
export const verifyMessage = async (profileId: string, messageId: string, messageContent: string, signatureEncoded: string): Promise<{ messageId: string, trusted: boolean }> => {
75+
let encoded = new TextEncoder().encode(messageContent);
76+
const publicKey = await getPublicKey(profileId);
77+
// base64 decode the string to get the binary data
78+
const binaryDerString = window.atob(signatureEncoded);
79+
// convert from a binary string to an ArrayBuffer
80+
const signature = str2ab(binaryDerString);
81+
82+
if (publicKey) {
83+
const trusted = await window.crypto.subtle.verify(
84+
{
85+
name: "RSA-PSS",
86+
saltLength: 32,
87+
},
88+
publicKey,
89+
signature,
90+
encoded
91+
);
92+
return { messageId, trusted };
93+
}
94+
95+
return { messageId, trusted: false };
96+
}
97+
98+
const getPublicKey = async (profileId: string): Promise<CryptoKey | undefined> => {
99+
const profileResourceUrl = removeHashFromUrl(profileId);
100+
await rdfStore.fetcher.load(profileResourceUrl);
101+
const pubKeyEncoded = extractObjectLastValue(rdfStore, PODCHAT.RSAPublicKey, profileResourceUrl, rdfStore.cache.sym(SIOC.content_encoded));
102+
if (pubKeyEncoded) {
103+
return importPublicKey(pubKeyEncoded);
104+
}
105+
106+
return undefined;
107+
}
108+
109+
const getPrivateKey = async (rsaPrivateKeyResourceUrl: string): Promise<CryptoKey | undefined> => {
110+
await rdfStore.fetcher.load(rsaPrivateKeyResourceUrl);
111+
const privKeyEncoded = extractObjectLastValue(rdfStore, PODCHAT.RSAPrivateKey, rsaPrivateKeyResourceUrl, rdfStore.cache.sym(SIOC.content_encoded));
112+
if (privKeyEncoded) {
113+
return importPrivateKey(privKeyEncoded);
114+
}
115+
return undefined;
116+
}
117+
118+
/*
119+
Import a PEM encoded RSA private key, to use for RSA-PSS signing.
120+
Takes a string containing the PEM encoded key, and returns a Promise
121+
that will resolve to a CryptoKey representing the private key.
122+
*/
123+
function importPrivateKey(pem: string) {
124+
// base64 decode the string to get the binary data
125+
const binaryDerString = window.atob(pem);
126+
// convert from a binary string to an ArrayBuffer
127+
const binaryDer = str2ab(binaryDerString);
128+
129+
return window.crypto.subtle.importKey(
130+
"pkcs8",
131+
binaryDer,
132+
{
133+
name: "RSA-PSS",
134+
// Consider using a 4096-bit key for systems that require long-term security
135+
//modulusLength: 4096,
136+
//publicExponent: new Uint8Array([1, 0, 1]),
137+
hash: "SHA-256",
138+
},
139+
true,
140+
["sign"]
141+
);
142+
}
143+
144+
/*
145+
Import a PEM encoded RSA public key, to use for RSA-OAEP encryption.
146+
Takes a string containing the PEM encoded key, and returns a Promise
147+
that will resolve to a CryptoKey representing the public key.
148+
*/
149+
function importPublicKey(pem: string) {
150+
// base64 decode the string to get the binary data
151+
const binaryDerString = window.atob(pem);
152+
// convert from a binary string to an ArrayBuffer
153+
const binaryDer = str2ab(binaryDerString);
154+
155+
return window.crypto.subtle.importKey(
156+
"spki",
157+
binaryDer,
158+
{
159+
name: "RSA-PSS",
160+
hash: "SHA-256"
161+
},
162+
true,
163+
["verify"]
164+
);
165+
}
166+
167+
/* const createKeyPair = async (profileId: string, rsaPrivateKeyResourceUrl: string) => {
168+
const key = await window.crypto.subtle
169+
.generateKey(
170+
{
171+
name: "RSA-PSS",
172+
// Consider using a 4096-bit key for systems that require long-term security
173+
modulusLength: 4096,
174+
publicExponent: new Uint8Array([1, 0, 1]),
175+
hash: "SHA-256",
176+
},
177+
true,
178+
["sign", "verify"]
179+
);
180+
181+
await privKeyPkcs8Pem(key.privateKey, profileId, rsaPrivateKeyResourceUrl);
182+
await pubKeySpkiPem(key.publicKey, profileId);
183+
} */
184+
185+
async function privKeyPkcs8Pem(privKey: CryptoKey, profileId: string, rsaPrivateKeyResourceUrl: string) {
186+
const exported = await window.crypto.subtle.exportKey("pkcs8", privKey);
187+
const exportedAsString = ab2str(exported);
188+
const exportedAsBase64 = window.btoa(exportedAsString);
189+
const del: Statement[] = [];
190+
const ins: Statement[] = [];
191+
del.push(...rdfStore.cache.statementsMatching(
192+
rdfStore.cache.sym(PODCHAT.RSAPrivateKey),
193+
rdfStore.cache.sym(SIOC.content_encoded),
194+
undefined,
195+
rdfStore.cache.sym(rsaPrivateKeyResourceUrl)
196+
));
197+
ins.push(quad(
198+
rdfStore.cache.sym(PODCHAT.RSAPrivateKey),
199+
rdfStore.cache.sym(SIOC.content_encoded),
200+
literal(exportedAsBase64), // alain
201+
rdfStore.cache.sym(rsaPrivateKeyResourceUrl)
202+
));
203+
await rdfStore.updateManager.update(del, ins);
204+
await aclForResource(rsaPrivateKeyResourceUrl, profileId);
205+
}
206+
207+
async function aclForResource(resourceUrl: string, ownerId: string) {
208+
const ins: Statement[] = [];
209+
const aclResourceUrl = resourceUrl + '.acl';
210+
const graph = rdfStore.cache.sym(aclResourceUrl);
211+
const aclId = rdfStore.cache.sym(aclResourceUrl + '#ControlReadWrite');
212+
ins.push(quad(aclId, rdfStore.cache.sym(RDF.type), rdfStore.cache.sym(ACL.Authorization), graph));
213+
ins.push(quad(aclId, rdfStore.cache.sym(ACL.accessTo), rdfStore.cache.sym(resourceUrl), graph));
214+
[ACL.Control, ACL.Write, ACL.Read].forEach(mode => {
215+
ins.push(quad(aclId, rdfStore.cache.sym(ACL.mode), rdfStore.cache.sym(mode), graph));
216+
});
217+
ins.push(quad(aclId, rdfStore.cache.sym(ACL.agent), rdfStore.cache.sym(ownerId), graph));
218+
await rdfStore.updateManager.update([], ins);
219+
}
220+
221+
async function pubKeySpkiPem(pubKey: CryptoKey, profileId: string) {
222+
const exported = await window.crypto.subtle.exportKey("spki", pubKey);
223+
const exportedAsString = ab2str(exported);
224+
const exportedAsBase64 = window.btoa(exportedAsString);
225+
const profileResourceUrl = removeHashFromUrl(profileId);
226+
const del: Statement[] = [];
227+
const ins: Statement[] = [];
228+
del.push(...rdfStore.cache.statementsMatching(
229+
rdfStore.cache.sym(PODCHAT.RSAPublicKey),
230+
rdfStore.cache.sym(SIOC.content_encoded),
231+
undefined,
232+
rdfStore.cache.sym(profileResourceUrl)
233+
));
234+
ins.push(quad(
235+
rdfStore.cache.sym(PODCHAT.RSAPublicKey),
236+
rdfStore.cache.sym(SIOC.content_encoded),
237+
literal(exportedAsBase64), // alain
238+
rdfStore.cache.sym(profileResourceUrl)
239+
));
240+
await rdfStore.updateManager.update(del, ins);
241+
}
242+
243+
/*
244+
Convert an ArrayBuffer into a string
245+
*/
246+
function ab2str(buf: ArrayBuffer) {
247+
return String.fromCharCode.apply(null, new Uint8Array(buf) as unknown as number[]);
248+
}
249+
250+
function str2ab(str: string) {
251+
const buf = new ArrayBuffer(str.length);
252+
const bufView = new Uint8Array(buf);
253+
for (let i = 0, strLen = str.length; i < strLen; i++) {
254+
bufView[i] = str.charCodeAt(i);
255+
}
256+
return buf;
257+
}

src/chat/keys.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import {schnorr} from '@noble/curves/secp256k1'
2+
import {bytesToHex} from '@noble/hashes/utils'
3+
4+
export function generatePrivateKey(): string {
5+
return bytesToHex(schnorr.utils.randomPrivateKey())
6+
}
7+
8+
export function getPublicKey(privateKey: string): string {
9+
return bytesToHex(schnorr.getPublicKey(privateKey))
10+
}

0 commit comments

Comments
 (0)