diff --git a/.github/workflows/test-nodejs.yaml b/.github/workflows/test-nodejs.yaml index 2a62921c8..23a77a0f8 100644 --- a/.github/workflows/test-nodejs.yaml +++ b/.github/workflows/test-nodejs.yaml @@ -5,6 +5,7 @@ on: paths: - '.github/workflows/test-nodejs.yaml' - 'docs/examples/js/**' + - 'docs/examples/ts/**' - 'src/soter/**' - 'src/themis/**' - 'src/wrappers/themis/jsthemis/**' @@ -132,3 +133,62 @@ jobs: node secure_comparator_client.js kill -SIGTERM "$!" echo "ok" + - name: Test 'import' syntax + if: always() + run: | + cd $GITHUB_WORKSPACE/docs/examples/js/ + echo "Test import syntax..." + node import_module.mjs + echo "ok" + - name: Install Typescript deps + if: always() + run: | + cd $GITHUB_WORKSPACE/docs/examples/ts/ + npm install + npm run compile + - name: Test Typescript examples (Secure Keygen) + if: always() + run: | + cd $GITHUB_WORKSPACE/docs/examples/ts + echo "Test Secure Keygen..." + node secure_keygen.js + echo "ok" + - name: Test Typescript examples (Secure Cell) + if: always() + run: | + cd $GITHUB_WORKSPACE/docs/examples/ts + echo "Test Secure Cell..." + node secure_cell.js + echo "ok" + - name: Test Typescript examples (Secure Message) + if: always() + run: | + cd $GITHUB_WORKSPACE/docs/examples/ts + echo "Test Secure Message..." + alice=($(node secure_keygen.js | cut -c 15-)) + bob=($(node secure_keygen.js | cut -c 15-)) + enc=$(node secure_message.js enc "${alice[1]}" "${bob[2]}" message) + dec=$(node secure_message.js dec "${bob[1]}" "${alice[2]}" "$enc") + test "$dec" = "message" + echo "ok" + - name: Test Typescript examples (Secure Session) + if: always() + run: | + cd $GITHUB_WORKSPACE/docs/examples/ts + echo "Test Secure Session..." + node secure_session_server.js & + sleep 1 # give the server time to launch + node secure_session_client.js > output.txt + kill -SIGTERM "$!" + grep -q 'Hello server!!!' output.txt + echo "ok" + - name: Test Typescript examples (Secure Comparator) + if: always() + run: | + cd $GITHUB_WORKSPACE/docs/examples/ts + echo "Test Secure Comparator..." + node secure_comparator_server.js & + sleep 1 # give the server time to launch + node secure_comparator_client.js + kill -SIGTERM "$!" + echo "ok" \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 69ac99f96..fd1cecdb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,11 @@ Changes that are currently in development and have not been released yet. -## [0.15.2](https://github.com/cossacklabs/themis/releases/tag/0.15.2), October 03 2023 +## [0.15.2](https://github.com/cossacklabs/themis/releases/tag/0.15.2), November 24 2023 + +### JsThemis wrapper +- Added the ability to use the `import` syntax for the jsthemis module. +- Added a declaration file for TypeScript. ### Android, ReactNative wrappers Updated versions of dependencies. New minimum versions of iOS, Android are set. diff --git a/docs/examples/js/import_module.mjs b/docs/examples/js/import_module.mjs new file mode 100644 index 000000000..860fdb4ce --- /dev/null +++ b/docs/examples/js/import_module.mjs @@ -0,0 +1,11 @@ +import themis from 'jsthemis'; +const { SymmetricKey, KeyPair} = themis; + +let masterKey = new SymmetricKey() +console.log(masterKey); + +let keypair = new KeyPair() +let privateKey = keypair.private() +let publicKey = keypair.public() +console.log(privateKey); +console.log(publicKey); diff --git a/docs/examples/ts/README.md b/docs/examples/ts/README.md new file mode 100644 index 000000000..10bcd274c --- /dev/null +++ b/docs/examples/ts/README.md @@ -0,0 +1,35 @@ +# Examples for JsThemis using TypeScript + +## Install +``` +git clone https://github.com/cossacklabs/themis.git +cd docs/examples/ts +npm install +``` + +## Compile +``` +npm run compile +``` +The command will compile TypeScript code into Javascript. + +## Run +``` +node ./secure_keygen.js +node ./secure_cell.js +node ./secure_message.js enc private1 public2 message +node ./secure_message.js dec private2 public1 message +node ./secure_comparator_server.js +node ./secure_comparator_client.js +node ./secure_session_server.js +node ./secure_session_client.js +``` +`private1, private2, public1, public2` is the keys which can be generated by `secure_keygen.js` + +Just run command by command to see how examples work. + +## Clean +``` +npm run clean +``` +The command will delete all compiled JS files. diff --git a/docs/examples/ts/package.json b/docs/examples/ts/package.json new file mode 100644 index 000000000..7f3728c0d --- /dev/null +++ b/docs/examples/ts/package.json @@ -0,0 +1,17 @@ +{ + "name": "ts_examples_themis", + "version": "1.0.0", + "description": "", + "main": "secure_keygen.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "clean": "rm *.js", + "compile": "find . -maxdepth 1 -type f -name '*.ts' -exec ./node_modules/.bin/tsc {} \\;" + }, + "author": "", + "license": "ISC", + "dependencies": { + "@types/node": "^20.9.5", + "typescript": "^4.4" + } +} diff --git a/docs/examples/ts/secure_cell.ts b/docs/examples/ts/secure_cell.ts new file mode 100644 index 000000000..1c275ed60 --- /dev/null +++ b/docs/examples/ts/secure_cell.ts @@ -0,0 +1,49 @@ +import { SecureCellSeal, SecureCellTokenProtect, SecureCellContextImprint } from "jsthemis" + +const message = Buffer.from('Test Message Please Ignore', 'utf-8') +const context = Buffer.from('Secure Cell example code','utf-8') +const master_key = Buffer.from('bm8sIHRoaXMgaXMgbm90IGEgdmFsaWQgbWFzdGVyIGtleQ==', 'base64') +const passphrase = 'My Litte Secret: Passphrase Is Magic' + +console.log('# Secure Cell in Seal mode\n') +console.log('## Master key API\n') +const scellMK = SecureCellSeal.withKey(master_key) +const encrypted_message = scellMK.encrypt(message) +console.log('Encrypted: ' + Buffer.from(encrypted_message).toString('base64')) +const decrypted_message = scellMK.decrypt(encrypted_message) +console.log('Decrypted: ' + Buffer.from(decrypted_message).toString()) +console.log() + +const encrypted_message2 = Buffer.from('AAEBQAwAAAAQAAAAEQAAAC0fCd2mOIxlDUORXz8+qCKuHCXcDii4bMF8OjOCOqsKEdV4+Ga2xTHPMupFvg==', 'base64') +const decrypted_message2 = scellMK.decrypt(encrypted_message2) +console.log('Decrypted (simulator): ' + Buffer.from(decrypted_message2).toString()) +console.log() +console.log('## Passphrase API\n') + +const scellPW = SecureCellSeal.withPassphrase(passphrase) +const encrypted_message3 = scellPW.encrypt(message) +console.log('Encrypted: ' + Buffer.from(encrypted_message3).toString('base64')) +const decrypted_message3 = scellPW.decrypt(encrypted_message3) +console.log('Decrypted: ' + Buffer.from(decrypted_message3).toString()) +console.log() + +console.log('# Secure Cell in Token Protect mode\n') + +const scellTP = SecureCellTokenProtect.withKey(master_key) +const encrypted_message4 = scellTP.encrypt(message) +console.log('Encrypted: ' + Buffer.from(encrypted_message4.data).toString('base64')) +console.log('Auth token: ' + Buffer.from(encrypted_message4.token).toString('base64')) +const decrypted_message4 = scellTP.decrypt(encrypted_message4.data, encrypted_message4.token) +console.log('Decrypted: ' + Buffer.from(decrypted_message4).toString()) +console.log('') + +console.log('# Secure Cell in Context Imprint mode\n') +const scellCI = SecureCellContextImprint.withKey(master_key) +const encrypted_message5 = scellCI.encrypt(message, context) +console.log('Encrypted: ' + Buffer.from(encrypted_message5).toString('base64')) +const decrypted_message5 = scellCI.decrypt(encrypted_message5, context) +console.log('Decrypted: ' + Buffer.from(decrypted_message5).toString()) +console.log('') +console.log('SecureCell example code finished') + + diff --git a/docs/examples/ts/secure_comparator_client.ts b/docs/examples/ts/secure_comparator_client.ts new file mode 100644 index 000000000..029279982 --- /dev/null +++ b/docs/examples/ts/secure_comparator_client.ts @@ -0,0 +1,24 @@ +import { Socket } from 'net'; +import { SecureComparator } from 'jsthemis'; + +const comparator = new SecureComparator(Buffer.from("secret")); + +const client = new Socket(); +client.connect(1337, '127.0.0.1', () => { + console.log('Connected'); + client.write(comparator.beginCompare()); +}); + +client.on('data', (data: Buffer) => { + const d = comparator.proceedCompare(data); + if (!comparator.isCompareComplete()) { + client.write(d); + } else { + console.log(comparator.isMatch()); + client.destroy(); + } +}); + +client.on('close', () => { + console.log('Connection closed'); +}); diff --git a/docs/examples/ts/secure_comparator_server.ts b/docs/examples/ts/secure_comparator_server.ts new file mode 100644 index 000000000..86b654ad6 --- /dev/null +++ b/docs/examples/ts/secure_comparator_server.ts @@ -0,0 +1,17 @@ +import { createServer, Socket } from 'net'; +import { SecureComparator } from 'jsthemis'; + +const server = createServer((socket: Socket) => { + const comparator = new SecureComparator(Buffer.from("secret")); + + socket.on('data', (data: Buffer) => { + const d = comparator.proceedCompare(data); + socket.write(d); + if (comparator.isCompareComplete()) { + console.log(comparator.isMatch()); + socket.destroy(); + } + }); +}); + +server.listen(1337, '127.0.0.1'); diff --git a/docs/examples/ts/secure_keygen.ts b/docs/examples/ts/secure_keygen.ts new file mode 100644 index 000000000..10b9ae08f --- /dev/null +++ b/docs/examples/ts/secure_keygen.ts @@ -0,0 +1,8 @@ +import { KeyPair, SymmetricKey } from 'jsthemis'; + +const masterKey = new SymmetricKey() as Uint8Array; +console.log("master key: ", Buffer.from(masterKey).toString("base64")); + +const keypair = new KeyPair(); +console.log("private key: ", Buffer.from(keypair.private()).toString("base64")); +console.log("public key : ", Buffer.from(keypair.public()).toString("base64")); diff --git a/docs/examples/ts/secure_message.ts b/docs/examples/ts/secure_message.ts new file mode 100644 index 000000000..6d0f4014a --- /dev/null +++ b/docs/examples/ts/secure_message.ts @@ -0,0 +1,22 @@ +import { SecureMessage } from 'jsthemis'; + +if (process.argv.length == 6) { + const command = process.argv[2]; + const private_key = process.argv[3]; + const peer_public_key = process.argv[4]; + const secure_message = new SecureMessage( + Buffer.from(private_key, "base64"), + Buffer.from(peer_public_key, "base64")); + + if (command == "enc") { + const encrypted_message = secure_message.encrypt(Buffer.from(process.argv[5])); + console.log(Buffer.from(encrypted_message).toString("base64")); + } else if (command == "dec") { + const decrypted_message = secure_message.decrypt(Buffer.from(process.argv[5], "base64")); + console.log(Buffer.from(decrypted_message).toString("utf8")); + } else { + console.log("usage node secure_message.js "); + } +} else { + console.log("usage node secure_message.js "); +} \ No newline at end of file diff --git a/docs/examples/ts/secure_session_client.ts b/docs/examples/ts/secure_session_client.ts new file mode 100644 index 000000000..2ce19b8bf --- /dev/null +++ b/docs/examples/ts/secure_session_client.ts @@ -0,0 +1,43 @@ +import { Socket } from 'net'; +import { SecureSession } from 'jsthemis'; + +const session = new SecureSession( + Buffer.from("client"), + Buffer.from("UkVDMgAAAC3DZR2qAEbvO092R/IKXBttnf9dVSU65R+Fb4eNoyxxlzn2n4GR", "base64"), + (id: Uint8Array) => { + if (id.toString() === "server") { + return Buffer.from("VUVDMgAAAC30/vs+AwciK6egi82A9TkTydVuOzMFsJ9AkA0gMGyNH0tSu5Bk", "base64"); + } else if (id.toString() === "client") { + return Buffer.from("VUVDMgAAAC15KNjgAr1DQEw+So1oztUarO4Jw/CGgyehBRCbOxbpHrPBKO7s", "base64"); + } + return null; + } +); + +let retry_count = 5; + +const client: Socket = new Socket(); +client.connect(1337, '127.0.0.1', () => { + console.log('Connected'); + client.write(session.connectRequest()); +}); + +client.on('data', (data: Buffer) => { + const d = session.unwrap(data); + if (!session.isEstablished()) { + client.write(d); + } else { + if (d !== undefined) { + console.log(d.toString()); + } + if (retry_count--) { + client.write(session.wrap(Buffer.from("Hello server!!!"))); + } else { + client.destroy(); + } + } +}); + +client.on('close', () => { + console.log('Connection closed'); +}); diff --git a/docs/examples/ts/secure_session_server.ts b/docs/examples/ts/secure_session_server.ts new file mode 100644 index 000000000..ac5bdd3fc --- /dev/null +++ b/docs/examples/ts/secure_session_server.ts @@ -0,0 +1,30 @@ +import { createServer, Socket } from 'net'; +import { SecureSession } from 'jsthemis'; + +const server = createServer((socket: Socket) => { + const session = new SecureSession( + Buffer.from("server"), + Buffer.from("UkVDMgAAAC0U6AK7AAm6ha0cgHmovSTpZax01+icg9xwFlZAqqGWeGTqbHUt", "base64"), + (id: Uint8Array) => { + if (id.toString() === "server") { + return Buffer.from("VUVDMgAAAC30/vs+AwciK6egi82A9TkTydVuOzMFsJ9AkA0gMGyNH0tSu5Bk", "base64"); + } else if (id.toString() === "client") { + return Buffer.from("VUVDMgAAAC15KNjgAr1DQEw+So1oztUarO4Jw/CGgyehBRCbOxbpHrPBKO7s", "base64"); + } + return null; + } + ); + + socket.on('data', (data: Buffer) => { + if (!session.isEstablished()) { + const d = session.unwrap(data); + socket.write(d); + } else { + const d = session.unwrap(data); + console.log(d.toString()); + socket.write(session.wrap(d)); + } + }); +}); + +server.listen(1337, '127.0.0.1'); diff --git a/src/wrappers/themis/jsthemis/index.d.ts b/src/wrappers/themis/jsthemis/index.d.ts new file mode 100644 index 000000000..d0757fcac --- /dev/null +++ b/src/wrappers/themis/jsthemis/index.d.ts @@ -0,0 +1,60 @@ +export declare class KeyPair { + constructor(privateKey?: Uint8Array, publicKey?: Uint8Array); + public(): Uint8Array; + private(): Uint8Array; +} + +export declare class SymmetricKey { + constructor(); +} + +export declare class SecureCellSeal { + static withKey(key: SymmetricKey): SecureCellSeal; + static withPassphrase(passphrase: string): SecureCellSeal; + encrypt(plaintext: Uint8Array, context?: Uint8Array): Uint8Array; + decrypt(encrypted: Uint8Array, context?: Uint8Array): Uint8Array; +} + +export declare interface SecureCellTokenProtectResult { + data: Uint8Array; + token: Uint8Array; +} + +export declare class SecureCellTokenProtect { + constructor(key: SymmetricKey); + static withKey(key: SymmetricKey): SecureCellTokenProtect; + encrypt(plaintext: Uint8Array, context?: Uint8Array): SecureCellTokenProtectResult; + decrypt(encrypted: Uint8Array, token: Uint8Array, context?: Uint8Array): Uint8Array; +} + +export declare class SecureCellContextImprint { + static withKey(key: SymmetricKey): SecureCellContextImprint; + encrypt(plaintext: Uint8Array, context: Uint8Array): Uint8Array; + decrypt(encrypted: Uint8Array, context: Uint8Array): Uint8Array; +} + +export declare class SecureMessage { + constructor(privateKey: Uint8Array | null, publicKey: Uint8Array | null); + sign(message: Uint8Array): Uint8Array; + verify(message: Uint8Array): Uint8Array; + encrypt(message: Uint8Array): Uint8Array; + decrypt(message: Uint8Array): Uint8Array; +} + +type GetPublicKeyCallback = (peerID: Uint8Array) => Uint8Array | null; + +export declare class SecureSession { + constructor(peerID: Uint8Array, privateKey: Uint8Array, getPublicKeyCallback: GetPublicKeyCallback); + isEstablished(): boolean; + connectRequest(): Uint8Array; + wrap(message: Uint8Array): Uint8Array; + unwrap(message: Uint8Array): Uint8Array; +} + +export declare class SecureComparator { + constructor(sharedSecret: Uint8Array); + beginCompare(): Uint8Array; + proceedCompare(request: Uint8Array): Uint8Array; + isCompareComplete(): boolean; + isMatch(): boolean; +} diff --git a/src/wrappers/themis/jsthemis/index.js b/src/wrappers/themis/jsthemis/index.js new file mode 100644 index 000000000..b23055a29 --- /dev/null +++ b/src/wrappers/themis/jsthemis/index.js @@ -0,0 +1,4 @@ +module.exports = { + ...require('./build/Release/jsthemis.node') +}; + diff --git a/src/wrappers/themis/jsthemis/package.json b/src/wrappers/themis/jsthemis/package.json index 43a5e25b0..83618229a 100644 --- a/src/wrappers/themis/jsthemis/package.json +++ b/src/wrappers/themis/jsthemis/package.json @@ -1,8 +1,8 @@ { "name": "jsthemis", - "version": "0.15.0", + "version": "0.15.2", "description": "Themis is a convenient cryptographic library for data protection.", - "main": "build/Release/jsthemis.node", + "main": "index.js", "scripts": { "test": "mocha", "preuninstall": "rm -rf build/*",