mirror of
https://github.com/gchq/CyberChef
synced 2025-12-05 23:53:27 +00:00
Compare commits
311 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d148cae814 | ||
|
|
f22211ce8c | ||
|
|
f9c24f2528 | ||
|
|
6f6786d79e | ||
|
|
a5ea7f7d58 | ||
|
|
d637ac7633 | ||
|
|
c1ad2386ef | ||
|
|
b0b6de116d | ||
|
|
1b161f997b | ||
|
|
b99af58636 | ||
|
|
d4edbb3c3e | ||
|
|
1b765605ca | ||
|
|
8286dc26ad | ||
|
|
19a438c15b | ||
|
|
12898a1a8e | ||
|
|
541f2a2988 | ||
|
|
d0277dde3f | ||
|
|
d184e40116 | ||
|
|
596db07647 | ||
|
|
c233c5c67e | ||
|
|
db788b57e7 | ||
|
|
c0983654d8 | ||
|
|
30bf95f2c1 | ||
|
|
da178107f9 | ||
|
|
4dda1d9e49 | ||
|
|
e11aec64cd | ||
|
|
71575e49d7 | ||
|
|
393d070b05 | ||
|
|
d7e4c28cd0 | ||
|
|
ccf2348cd6 | ||
|
|
cde3eb2c39 | ||
|
|
f29d8eeda8 | ||
|
|
daee7ac761 | ||
|
|
313d1a580e | ||
|
|
ca9bab5d4c | ||
|
|
e35ef8f39b | ||
|
|
e709582062 | ||
|
|
a6732ba815 | ||
|
|
466d872d30 | ||
|
|
3cf7238106 | ||
|
|
f8d08cc5db | ||
|
|
c1bdca8df3 | ||
|
|
2be2c83f67 | ||
|
|
a271eaabd0 | ||
|
|
1d130c88a8 | ||
|
|
6f5018d45e | ||
|
|
f51ee76c72 | ||
|
|
0c9e8fe050 | ||
|
|
8f41571e47 | ||
|
|
6d14368e2f | ||
|
|
f90ad48906 | ||
|
|
e95dac82c2 | ||
|
|
144601ffd4 | ||
|
|
cbcc2aa731 | ||
|
|
f9354c8cd1 | ||
|
|
7a4f418e75 | ||
|
|
8fa8e34027 | ||
|
|
5225874498 | ||
|
|
802493fec4 | ||
|
|
01f0625d6a | ||
|
|
38ff7ec89f | ||
|
|
7163a0802d | ||
|
|
8d0fcf37c5 | ||
|
|
3da5a8bb34 | ||
|
|
fad33b583b | ||
|
|
8f450501cc | ||
|
|
b3ae0e577a | ||
|
|
aedac94e40 | ||
|
|
08c5dbce09 | ||
|
|
482d658de7 | ||
|
|
39e34081fc | ||
|
|
5797786a75 | ||
|
|
f6977ea264 | ||
|
|
cdc15c0f20 | ||
|
|
18408901be | ||
|
|
982c915931 | ||
|
|
a339eacd45 | ||
|
|
8fc0e012e3 | ||
|
|
b7fb9635e5 | ||
|
|
f988a958bb | ||
|
|
c80cb57b07 | ||
|
|
dec28e16d4 | ||
|
|
525cb0689f | ||
|
|
7796c473ae | ||
|
|
8445165491 | ||
|
|
c5698fcd65 | ||
|
|
786e50c3c3 | ||
|
|
7d03be3a77 | ||
|
|
8b12caad78 | ||
|
|
e1492c3bb1 | ||
|
|
3cc66e9db9 | ||
|
|
27677adbe8 | ||
|
|
6fa06a4f8b | ||
|
|
a3be4d2945 | ||
|
|
3dc5b5c31a | ||
|
|
342e11f83e | ||
|
|
b6d78b4001 | ||
|
|
2aaa6db538 | ||
|
|
fc909d8199 | ||
|
|
262136393b | ||
|
|
1640859542 | ||
|
|
f0b48acaf9 | ||
|
|
bb8c305fc8 | ||
|
|
3950dba2c5 | ||
|
|
903ea45228 | ||
|
|
b116b8ba1e | ||
|
|
908043fb7f | ||
|
|
2d7e3f180e | ||
|
|
c813d17595 | ||
|
|
7d16265c4e | ||
|
|
4fb4764d3f | ||
|
|
2385f1cbf8 | ||
|
|
3d80d66925 | ||
|
|
ce208b69fe | ||
|
|
a44418c6a1 | ||
|
|
5ac84491c1 | ||
|
|
58769eb06e | ||
|
|
59ae9c6437 | ||
|
|
81d698c091 | ||
|
|
bce0895392 | ||
|
|
e909eea82a | ||
|
|
ef4b977bef | ||
|
|
8707287349 | ||
|
|
328c0ade22 | ||
|
|
b3d92b04cb | ||
|
|
a5703cb4f1 | ||
|
|
bb5b92571e | ||
|
|
3ad5f889a0 | ||
|
|
3ff10bfeae | ||
|
|
c8a2a8b003 | ||
|
|
6acb3a7ca5 | ||
|
|
ef38897a01 | ||
|
|
33db0e666a | ||
|
|
cf32372a57 | ||
|
|
b98cf9538d | ||
|
|
768fef502d | ||
|
|
1a707eab86 | ||
|
|
e2efc3e8e8 | ||
|
|
fa6d5e60ce | ||
|
|
0a7a0ac681 | ||
|
|
981d4be44d | ||
|
|
80bdf7410b | ||
|
|
6f45d33c8a | ||
|
|
cd22985f11 | ||
|
|
66c0425080 | ||
|
|
fd7fd9ca35 | ||
|
|
7b3efa746e | ||
|
|
90ddc2bfa7 | ||
|
|
8e3425ed6d | ||
|
|
c1bb42fe65 | ||
|
|
b99f73919f | ||
|
|
978bf75765 | ||
|
|
432d5b43a1 | ||
|
|
3412372d1e | ||
|
|
ca6d472e5d | ||
|
|
c2e130f369 | ||
|
|
6501454424 | ||
|
|
0019a4e1db | ||
|
|
f8874fc586 | ||
|
|
4ae875601a | ||
|
|
e8880f068f | ||
|
|
45c1c23e09 | ||
|
|
e10d4bf45c | ||
|
|
369b640408 | ||
|
|
d2d30bf668 | ||
|
|
dcb59edb79 | ||
|
|
84d31c1d59 | ||
|
|
58d41f4458 | ||
|
|
360effb839 | ||
|
|
d923c99975 | ||
|
|
2b538061e9 | ||
|
|
0c9db5afe9 | ||
|
|
1031429550 | ||
|
|
4a7ea469d4 | ||
|
|
833c1cd98f | ||
|
|
662922be6f | ||
|
|
370ae323f6 | ||
|
|
514eef50de | ||
|
|
ec1fd7b923 | ||
|
|
737ce99398 | ||
|
|
4f1a897e18 | ||
|
|
588a8b2a3a | ||
|
|
f281a32a4e | ||
|
|
d09e6089ca | ||
|
|
7b6062a4a2 | ||
|
|
7975fadfe9 | ||
|
|
9fa7edffbf | ||
|
|
24a47445f6 | ||
|
|
9a0b784153 | ||
|
|
a446ec31c7 | ||
|
|
9323737d1d | ||
|
|
d288acdd4e | ||
|
|
765aded208 | ||
|
|
1f9fd92b01 | ||
|
|
2be642e4c9 | ||
|
|
934356dea8 | ||
|
|
a037bf6f9d | ||
|
|
c005c86c27 | ||
|
|
68278267e1 | ||
|
|
da2d5674a5 | ||
|
|
5bb8eb22ec | ||
|
|
0d86a7e427 | ||
|
|
9f4aa0a123 | ||
|
|
da838e266e | ||
|
|
fd160e87e8 | ||
|
|
0dd4304902 | ||
|
|
a0b94bba4e | ||
|
|
74c2a2b5cb | ||
|
|
b691c30677 | ||
|
|
01acefe4cf | ||
|
|
1a2c5a95c7 | ||
|
|
eb8725a0db | ||
|
|
57e1061063 | ||
|
|
91f4681a3c | ||
|
|
8148c1a8a8 | ||
|
|
ce61bcc078 | ||
|
|
49e2b05a11 | ||
|
|
481f2a4717 | ||
|
|
c01c076561 | ||
|
|
2391e08ac1 | ||
|
|
a7a2fe243a | ||
|
|
b5c655dd70 | ||
|
|
d84a61d108 | ||
|
|
13abbd2c5d | ||
|
|
d22eac9f35 | ||
|
|
d71aa8d7e2 | ||
|
|
4db6199fd9 | ||
|
|
dd9cbbac77 | ||
|
|
069d4956aa | ||
|
|
61fee3122a | ||
|
|
5a8255a9f4 | ||
|
|
5a2a8b4c8e | ||
|
|
1079080f5c | ||
|
|
1e914c4cf2 | ||
|
|
53226c1050 | ||
|
|
7a4e0301d2 | ||
|
|
42826e542d | ||
|
|
42d1c9403c | ||
|
|
ab43635583 | ||
|
|
b8ecd83bfd | ||
|
|
58a8af20a6 | ||
|
|
75a58f465c | ||
|
|
220053c044 | ||
|
|
613cbaa556 | ||
|
|
0d0a634255 | ||
|
|
105090db60 | ||
|
|
3e9c75f735 | ||
|
|
d42075072b | ||
|
|
6a099f0813 | ||
|
|
9af5e40071 | ||
|
|
4bf2a29070 | ||
|
|
069d0e48c1 | ||
|
|
c8cb2692dd | ||
|
|
acb8c0b5af | ||
|
|
0c14bacea7 | ||
|
|
87e956fe7d | ||
|
|
02b9dbdee9 | ||
|
|
cd2c8078c8 | ||
|
|
ffc4b0a0a8 | ||
|
|
eee92aa1aa | ||
|
|
74eb4cca86 | ||
|
|
49f5c94a75 | ||
|
|
d94e8c8187 | ||
|
|
2307325af8 | ||
|
|
4e57b4be88 | ||
|
|
21335e7d05 | ||
|
|
78768e00d4 | ||
|
|
3eb44708e5 | ||
|
|
c077b22410 | ||
|
|
8c757d1e03 | ||
|
|
e74c86842b | ||
|
|
ad6e30f3d4 | ||
|
|
b6eb4e84e4 | ||
|
|
2a6db47aeb | ||
|
|
19b7957523 | ||
|
|
1b1a3c261d | ||
|
|
0d2cb02f97 | ||
|
|
7d8d80ca2c | ||
|
|
b4e130234c | ||
|
|
53cc413eae | ||
|
|
0449c46b38 | ||
|
|
f8450183c1 | ||
|
|
eb1f8b4bc6 | ||
|
|
088864fd9c | ||
|
|
cd0c86e0d6 | ||
|
|
a56f92cdee | ||
|
|
4c285bce57 | ||
|
|
ede75530d0 | ||
|
|
3ae225ac59 | ||
|
|
fd07b89028 | ||
|
|
0198f05112 | ||
|
|
729307336e | ||
|
|
f4f9b5c91c | ||
|
|
f355fe3447 | ||
|
|
321718d43a | ||
|
|
8d3836cb16 | ||
|
|
9829491c4c | ||
|
|
e6fb0be1d0 | ||
|
|
d02124550b | ||
|
|
6aa9d2b492 | ||
|
|
6784a1c027 | ||
|
|
39ab600887 | ||
|
|
49ea532cdc | ||
|
|
247e9bfbde | ||
|
|
5944568565 | ||
|
|
1c87707a76 | ||
|
|
b4188db671 | ||
|
|
dc642be1f5 | ||
|
|
6cdc7d3966 | ||
|
|
281d558111 | ||
|
|
fa89713f19 |
@@ -1 +1,2 @@
|
||||
src/core/vendor/**
|
||||
src/web/static/clippy_assets/**
|
||||
@@ -1,10 +1,12 @@
|
||||
{
|
||||
"parser": "babel-eslint",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 9,
|
||||
"ecmaFeatures": {
|
||||
"impliedStrict": true
|
||||
},
|
||||
"sourceType": "module"
|
||||
"sourceType": "module",
|
||||
"allowImportExportEverywhere": true
|
||||
},
|
||||
"env": {
|
||||
"browser": true,
|
||||
@@ -100,6 +102,7 @@
|
||||
"$": false,
|
||||
"jQuery": false,
|
||||
"log": false,
|
||||
"app": false,
|
||||
|
||||
"COMPILE_TIME": false,
|
||||
"COMPILE_MSG": false,
|
||||
|
||||
15
.github/ISSUE_TEMPLATE.md
vendored
15
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,14 +1 @@
|
||||
<!-- Prefix the title above with one of the following: -->
|
||||
<!-- Bug report: -->
|
||||
<!-- Operation request: -->
|
||||
<!-- Feature request: -->
|
||||
<!-- Misc: -->
|
||||
|
||||
### Summary
|
||||
|
||||
|
||||
### Example
|
||||
<!-- If describing a bug, tell us what happens instead of the expected behavior -->
|
||||
<!-- Include a link that triggers the bug if possible -->
|
||||
<!-- If you are requesting a new operation, include example input and output -->
|
||||
|
||||
<!-- Prefix the title above with 'Misc:' -->
|
||||
|
||||
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: 'Bug report: <Insert title here>'
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- Prefix the title above with 'Bug report:' -->
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior or a link to the recipe / input used to cause the bug:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (if relevant, please complete the following information):**
|
||||
- OS: [e.g. Windows]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for the project
|
||||
title: 'Feature request: <Insert title here>'
|
||||
labels: feature
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
<!-- Prefix the title above with 'Feature request:' -->
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
16
.github/ISSUE_TEMPLATE/operation-request.md
vendored
Normal file
16
.github/ISSUE_TEMPLATE/operation-request.md
vendored
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
name: Operation request
|
||||
about: Suggest a new operation
|
||||
title: 'Operation request: <Insert title here>'
|
||||
labels: operation
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- Prefix the title above with 'Operation request:' -->
|
||||
|
||||
## Summary
|
||||
|
||||
### Example Input
|
||||
|
||||
### Example Output
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -6,6 +6,8 @@ docs/*
|
||||
!docs/*.conf.json
|
||||
!docs/*.ico
|
||||
.vscode
|
||||
.*.swp
|
||||
.DS_Store
|
||||
src/core/config/modules/*
|
||||
src/core/config/OperationConfig.json
|
||||
src/core/operations/index.mjs
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- node
|
||||
- lts/*
|
||||
addons:
|
||||
chrome: stable
|
||||
install: npm install
|
||||
@@ -30,8 +30,9 @@ deploy:
|
||||
skip_cleanup: true
|
||||
api_key:
|
||||
secure: "HV1WSKv4l/0Y2bKKs1iBJocBcmLj08PCRUeEM/jTwA4jqJ8EiLHWiXtER/D5sEg2iibRVKd2OQjfrmS6bo4AiwdeVgAKmv0FtS2Jw+391N8Nd5AkEANHa5Om/IpHLTL2YRAjpJTsDpY72bMUTJIwjQA3TFJkgrpOw6KYfohOcgbxLpZ4XuNJRU3VL4Hsxdv5V9aOVmfFOmMOVPQlakXy7NgtW5POp1f2WJwgcZxylkR1CjwaqMyXmSoVl46pyH3tr5+dptsQoKSGdi6sIHGA60oDotFPcm+0ifa47wZw+vapuuDi4tdNxhrHGaDMG8xiE0WFDHwQUDlk2/+W7j9SEX0H3Em7us371JXRp56EDwEcDa34VpVkC6i8HGcHK55hnxVbMZXGf3qhOFD8wY7qMbjMRvIpucrMHBi86OfkDfv0vDj2LyvIl5APj/AX50BrE0tfH1MZbH26Jkx4NdlkcxQ14GumarmUqfmVvbX/fsoA6oUuAAE9ZgRRi3KHO4wci6KUcRfdm+XOeUkaBFsL86G3EEYIvrtBTuaypdz+Cx7nd1iPZyWMx5Y1gXnVzha4nBdV4+7l9JIsFggD8QVpw2uHXQiS1KXFjOeqA3DBD8tjMB7q26Fl2fD3jkOo4BTbQ2NrRIZUu/iL+fOmMPsyMt2qulB0yaSBCfkbEq8xrUA="
|
||||
file_glob: true
|
||||
file:
|
||||
- build/prod/cyberchef.htm
|
||||
- build/prod/*.zip
|
||||
- build/node/CyberChef.js
|
||||
on:
|
||||
repo: gchq/CyberChef
|
||||
|
||||
66
CHANGELOG.md
66
CHANGELOG.md
@@ -2,7 +2,44 @@
|
||||
All major and minor version changes will be documented in this file. Details of patch-level version changes can be found in [commit messages](https://github.com/gchq/CyberChef/commits/master).
|
||||
|
||||
|
||||
### [8.24.0] - 2019-01-18
|
||||
### [8.34.0] - 2019-06-28
|
||||
- Various new visualisations added to the 'Entropy' operation [@MShwed] | [#535]
|
||||
- Efficiency improvements made to the 'Entropy' operation for large file support [@n1474335]
|
||||
|
||||
### [8.33.0] - 2019-06-27
|
||||
- 'Bzip2 Compress' operation added and 'Bzip2 Decompress' operation greatly improved [@artemisbot] | [#531]
|
||||
|
||||
### [8.32.0] - 2019-06-27
|
||||
- 'Indec of Coincidence' operation added [@Ge0rg3] | [#571]
|
||||
|
||||
### [8.31.0] - 2019-04-12
|
||||
- The downloadable version of CyberChef is now a .zip file containing separate modules rather than a single .htm file. It is still completely standalone and will not make any external network requests. This change reduces the complexity of the build process significantly. [@n1474335]
|
||||
|
||||
### [8.30.0] - 2019-04-12
|
||||
- 'Decode Protobuf' operation added [@n1474335] | [#533]
|
||||
|
||||
### [8.29.0] - 2019-03-31
|
||||
- 'BLAKE2s' and 'BLAKE2b' hashing operations added [@h345983745] | [#525]
|
||||
|
||||
### [8.28.0] - 2019-03-31
|
||||
- 'Heatmap Chart', 'Hex Density Chart', 'Scatter Chart' and 'Series Chart' operation added [@artemisbot] [@tlwr] | [#496] [#143]
|
||||
|
||||
### [8.27.0] - 2019-03-14
|
||||
- 'Enigma', 'Typex', 'Bombe' and 'Multiple Bombe' operations added [@s2224834] | [#516]
|
||||
- See [this wiki article](https://github.com/gchq/CyberChef/wiki/Enigma,-the-Bombe,-and-Typex) for a full explanation of these operations.
|
||||
- New Bombe-style loading animation added for long-running operations [@n1474335]
|
||||
- New operation argument types added: `populateMultiOption` and `argSelector` [@n1474335]
|
||||
|
||||
### [8.26.0] - 2019-03-09
|
||||
- Various image manipulation operations added [@j433866] | [#506]
|
||||
|
||||
### [8.25.0] - 2019-03-09
|
||||
- 'Extract Files' operation added and more file formats supported [@n1474335] | [#440]
|
||||
|
||||
### [8.24.0] - 2019-02-08
|
||||
- 'DNS over HTTPS' operation added [@h345983745] | [#489]
|
||||
|
||||
### [8.23.1] - 2019-01-18
|
||||
- 'Convert co-ordinate format' operation added [@j433866] | [#476]
|
||||
|
||||
### [8.23.0] - 2019-01-18
|
||||
@@ -103,7 +140,18 @@ All major and minor version changes will be documented in this file. Details of
|
||||
|
||||
|
||||
|
||||
[8.34.0]: https://github.com/gchq/CyberChef/releases/tag/v8.34.0
|
||||
[8.33.0]: https://github.com/gchq/CyberChef/releases/tag/v8.33.0
|
||||
[8.32.0]: https://github.com/gchq/CyberChef/releases/tag/v8.32.0
|
||||
[8.31.0]: https://github.com/gchq/CyberChef/releases/tag/v8.31.0
|
||||
[8.30.0]: https://github.com/gchq/CyberChef/releases/tag/v8.30.0
|
||||
[8.29.0]: https://github.com/gchq/CyberChef/releases/tag/v8.29.0
|
||||
[8.28.0]: https://github.com/gchq/CyberChef/releases/tag/v8.28.0
|
||||
[8.27.0]: https://github.com/gchq/CyberChef/releases/tag/v8.27.0
|
||||
[8.26.0]: https://github.com/gchq/CyberChef/releases/tag/v8.26.0
|
||||
[8.25.0]: https://github.com/gchq/CyberChef/releases/tag/v8.25.0
|
||||
[8.24.0]: https://github.com/gchq/CyberChef/releases/tag/v8.24.0
|
||||
[8.23.1]: https://github.com/gchq/CyberChef/releases/tag/v8.23.1
|
||||
[8.23.0]: https://github.com/gchq/CyberChef/releases/tag/v8.23.0
|
||||
[8.22.0]: https://github.com/gchq/CyberChef/releases/tag/v8.22.0
|
||||
[8.21.0]: https://github.com/gchq/CyberChef/releases/tag/v8.21.0
|
||||
@@ -137,7 +185,10 @@ All major and minor version changes will be documented in this file. Details of
|
||||
[@d98762625]: https://github.com/d98762625
|
||||
[@j433866]: https://github.com/j433866
|
||||
[@GCHQ77703]: https://github.com/GCHQ77703
|
||||
[@h345983745]: https://github.com/h345983745
|
||||
[@s2224834]: https://github.com/s2224834
|
||||
[@artemisbot]: https://github.com/artemisbot
|
||||
[@tlwr]: https://github.com/tlwr
|
||||
[@picapi]: https://github.com/picapi
|
||||
[@Dachande663]: https://github.com/Dachande663
|
||||
[@JustAnotherMark]: https://github.com/JustAnotherMark
|
||||
@@ -151,9 +202,12 @@ All major and minor version changes will be documented in this file. Details of
|
||||
[@Cynser]: https://github.com/Cynser
|
||||
[@anthony-arnold]: https://github.com/anthony-arnold
|
||||
[@masq]: https://github.com/masq
|
||||
[@Ge0rg3]: https://github.com/Ge0rg3
|
||||
[@MShwed]: https://github.com/MShwed
|
||||
|
||||
[#95]: https://github.com/gchq/CyberChef/pull/299
|
||||
[#173]: https://github.com/gchq/CyberChef/pull/173
|
||||
[#143]: https://github.com/gchq/CyberChef/pull/143
|
||||
[#224]: https://github.com/gchq/CyberChef/pull/224
|
||||
[#239]: https://github.com/gchq/CyberChef/pull/239
|
||||
[#248]: https://github.com/gchq/CyberChef/pull/248
|
||||
@@ -175,6 +229,7 @@ All major and minor version changes will be documented in this file. Details of
|
||||
[#394]: https://github.com/gchq/CyberChef/pull/394
|
||||
[#428]: https://github.com/gchq/CyberChef/pull/428
|
||||
[#439]: https://github.com/gchq/CyberChef/pull/439
|
||||
[#440]: https://github.com/gchq/CyberChef/pull/440
|
||||
[#441]: https://github.com/gchq/CyberChef/pull/441
|
||||
[#443]: https://github.com/gchq/CyberChef/pull/443
|
||||
[#446]: https://github.com/gchq/CyberChef/pull/446
|
||||
@@ -186,3 +241,12 @@ All major and minor version changes will be documented in this file. Details of
|
||||
[#467]: https://github.com/gchq/CyberChef/pull/467
|
||||
[#468]: https://github.com/gchq/CyberChef/pull/468
|
||||
[#476]: https://github.com/gchq/CyberChef/pull/476
|
||||
[#489]: https://github.com/gchq/CyberChef/pull/489
|
||||
[#496]: https://github.com/gchq/CyberChef/pull/496
|
||||
[#506]: https://github.com/gchq/CyberChef/pull/506
|
||||
[#516]: https://github.com/gchq/CyberChef/pull/516
|
||||
[#525]: https://github.com/gchq/CyberChef/pull/525
|
||||
[#531]: https://github.com/gchq/CyberChef/pull/531
|
||||
[#533]: https://github.com/gchq/CyberChef/pull/533
|
||||
[#535]: https://github.com/gchq/CyberChef/pull/535
|
||||
[#571]: https://github.com/gchq/CyberChef/pull/571
|
||||
|
||||
131
Gruntfile.js
131
Gruntfile.js
@@ -4,7 +4,6 @@ const webpack = require("webpack");
|
||||
const HtmlWebpackPlugin = require("html-webpack-plugin");
|
||||
const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin;
|
||||
const NodeExternals = require("webpack-node-externals");
|
||||
const Inliner = require("web-resource-inliner");
|
||||
const glob = require("glob");
|
||||
const path = require("path");
|
||||
|
||||
@@ -43,18 +42,16 @@ module.exports = function (grunt) {
|
||||
|
||||
grunt.registerTask("prod",
|
||||
"Creates a production-ready build. Use the --msg flag to add a compile message.",
|
||||
["eslint", "clean:prod", "clean:config", "exec:generateConfig", "webpack:web", "inline", "chmod"]);
|
||||
[
|
||||
"eslint", "clean:prod", "clean:config", "exec:generateConfig", "webpack:web",
|
||||
"copy:standalone", "zip:standalone", "clean:standalone", "chmod"
|
||||
]);
|
||||
|
||||
grunt.registerTask("default",
|
||||
"Lints the code base",
|
||||
["eslint", "exec:repoSize"]);
|
||||
|
||||
grunt.registerTask("inline",
|
||||
"Compiles a production build of CyberChef into a single, portable web page.",
|
||||
["exec:generateConfig", "webpack:webInline", "runInliner", "clean:inlineScripts"]);
|
||||
|
||||
|
||||
grunt.registerTask("runInliner", runInliner);
|
||||
grunt.registerTask("doc", "docs");
|
||||
grunt.registerTask("tests", "test");
|
||||
grunt.registerTask("lint", "eslint");
|
||||
@@ -72,6 +69,7 @@ module.exports = function (grunt) {
|
||||
grunt.loadNpmTasks("grunt-accessibility");
|
||||
grunt.loadNpmTasks("grunt-concurrent");
|
||||
grunt.loadNpmTasks("grunt-contrib-connect");
|
||||
grunt.loadNpmTasks("grunt-zip");
|
||||
|
||||
|
||||
// Project configuration
|
||||
@@ -94,32 +92,6 @@ module.exports = function (grunt) {
|
||||
},
|
||||
moduleEntryPoints = listEntryModules();
|
||||
|
||||
/**
|
||||
* Compiles a production build of CyberChef into a single, portable web page.
|
||||
*/
|
||||
function runInliner() {
|
||||
const done = this.async();
|
||||
Inliner.html({
|
||||
relativeTo: "build/prod/",
|
||||
fileContent: grunt.file.read("build/prod/cyberchef.htm"),
|
||||
images: true,
|
||||
svgs: true,
|
||||
scripts: true,
|
||||
links: true,
|
||||
strict: true
|
||||
}, function(error, result) {
|
||||
if (error) {
|
||||
if (error instanceof Error) {
|
||||
done(error);
|
||||
} else {
|
||||
done(new Error(error));
|
||||
}
|
||||
} else {
|
||||
grunt.file.write("build/prod/cyberchef.htm", result);
|
||||
done(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an entry list for all the modules.
|
||||
@@ -130,7 +102,7 @@ module.exports = function (grunt) {
|
||||
glob.sync("./src/core/config/modules/*.mjs").forEach(file => {
|
||||
const basename = path.basename(file);
|
||||
if (basename !== "Default.mjs" && basename !== "OpModules.mjs")
|
||||
entryModules[basename.split(".mjs")[0]] = path.resolve(file);
|
||||
entryModules["modules/" + basename.split(".mjs")[0]] = path.resolve(file);
|
||||
});
|
||||
|
||||
return entryModules;
|
||||
@@ -143,7 +115,7 @@ module.exports = function (grunt) {
|
||||
node: ["build/node/*"],
|
||||
config: ["src/core/config/OperationConfig.json", "src/core/config/modules/*", "src/code/operations/index.mjs"],
|
||||
docs: ["docs/*", "!docs/*.conf.json", "!docs/*.ico", "!docs/*.png"],
|
||||
inlineScripts: ["build/prod/scripts.js"],
|
||||
standalone: ["build/prod/CyberChef*.html"]
|
||||
},
|
||||
eslint: {
|
||||
options: {
|
||||
@@ -151,7 +123,7 @@ module.exports = function (grunt) {
|
||||
},
|
||||
configs: ["*.{js,mjs}"],
|
||||
core: ["src/core/**/*.{js,mjs}", "!src/core/vendor/**/*", "!src/core/operations/legacy/**/*"],
|
||||
web: ["src/web/**/*.{js,mjs}"],
|
||||
web: ["src/web/**/*.{js,mjs}", "!src/web/static/**/*"],
|
||||
node: ["src/node/**/*.{js,mjs}"],
|
||||
tests: ["tests/**/*.{js,mjs}"],
|
||||
},
|
||||
@@ -194,7 +166,11 @@ module.exports = function (grunt) {
|
||||
sitemap: "./src/web/static/sitemap.js"
|
||||
}, moduleEntryPoints),
|
||||
output: {
|
||||
path: __dirname + "/build/prod"
|
||||
path: __dirname + "/build/prod",
|
||||
filename: chunkData => {
|
||||
return chunkData.chunk.name === "main" ? "assets/[name].js": "[name].js";
|
||||
},
|
||||
globalObject: "this"
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
@@ -224,33 +200,6 @@ module.exports = function (grunt) {
|
||||
]
|
||||
};
|
||||
},
|
||||
webInline: {
|
||||
mode: "production",
|
||||
target: "web",
|
||||
entry: "./src/web/index.js",
|
||||
output: {
|
||||
filename: "scripts.js",
|
||||
path: __dirname + "/build/prod"
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin(Object.assign({}, BUILD_CONSTANTS, {
|
||||
INLINE: "true"
|
||||
})),
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "cyberchef.htm",
|
||||
template: "./src/web/html/index.html",
|
||||
compileTime: compileTime,
|
||||
version: pkg.version + "s",
|
||||
inline: true,
|
||||
minify: {
|
||||
removeComments: true,
|
||||
collapseWhitespace: true,
|
||||
minifyJS: true,
|
||||
minifyCSS: true
|
||||
}
|
||||
}),
|
||||
]
|
||||
},
|
||||
node: {
|
||||
mode: "production",
|
||||
target: "node",
|
||||
@@ -283,7 +232,8 @@ module.exports = function (grunt) {
|
||||
warningsFilter: [
|
||||
/source-map/,
|
||||
/dependency is an expression/,
|
||||
/export 'default'/
|
||||
/export 'default'/,
|
||||
/Can't resolve 'sodium'/
|
||||
],
|
||||
}
|
||||
},
|
||||
@@ -299,6 +249,9 @@ module.exports = function (grunt) {
|
||||
"./config/modules/OpModules": "./config/modules/Default"
|
||||
}
|
||||
},
|
||||
output: {
|
||||
globalObject: "this",
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin(BUILD_CONSTANTS),
|
||||
new HtmlWebpackPlugin({
|
||||
@@ -312,6 +265,18 @@ module.exports = function (grunt) {
|
||||
}
|
||||
}
|
||||
},
|
||||
zip: {
|
||||
standalone: {
|
||||
cwd: "build/prod/",
|
||||
src: [
|
||||
"build/prod/**/*",
|
||||
"!build/prod/index.html",
|
||||
"!build/prod/BundleAnalyzerReport.html",
|
||||
"!build/prod/sitemap.js"
|
||||
],
|
||||
dest: `build/prod/CyberChef_v${pkg.version}.zip`
|
||||
}
|
||||
},
|
||||
connect: {
|
||||
prod: {
|
||||
options: {
|
||||
@@ -324,10 +289,16 @@ module.exports = function (grunt) {
|
||||
ghPages: {
|
||||
options: {
|
||||
process: function (content, srcpath) {
|
||||
// Add Google Analytics code to index.html
|
||||
if (srcpath.indexOf("index.html") >= 0) {
|
||||
// Add Google Analytics code to index.html
|
||||
content = content.replace("</body></html>",
|
||||
grunt.file.read("src/web/static/ga.html") + "</body></html>");
|
||||
|
||||
// Add Structured Data for SEO
|
||||
content = content.replace("</head>",
|
||||
"<script type='application/ld+json'>" +
|
||||
JSON.stringify(JSON.parse(grunt.file.read("src/web/static/structuredData.json"))) +
|
||||
"</script></head>");
|
||||
return grunt.template.process(content, srcpath);
|
||||
} else {
|
||||
return content;
|
||||
@@ -346,6 +317,28 @@ module.exports = function (grunt) {
|
||||
dest: "build/prod/"
|
||||
},
|
||||
]
|
||||
},
|
||||
standalone: {
|
||||
options: {
|
||||
process: function (content, srcpath) {
|
||||
if (srcpath.indexOf("index.html") >= 0) {
|
||||
// Replace download link with version number
|
||||
content = content.replace(/<a [^>]+>Download CyberChef.+?<\/a>/,
|
||||
`<span>Version ${pkg.version}</span>`);
|
||||
|
||||
return grunt.template.process(content, srcpath);
|
||||
} else {
|
||||
return content;
|
||||
}
|
||||
},
|
||||
noProcess: ["**", "!**/*.html"]
|
||||
},
|
||||
files: [
|
||||
{
|
||||
src: "build/prod/index.html",
|
||||
dest: `build/prod/CyberChef_v${pkg.version}.html`
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
chmod: {
|
||||
@@ -391,9 +384,7 @@ module.exports = function (grunt) {
|
||||
generateConfig: {
|
||||
command: [
|
||||
"echo '\n--- Regenerating config files. ---'",
|
||||
"mkdir -p src/core/config/modules",
|
||||
"echo 'export default {};\n' > src/core/config/modules/OpModules.mjs",
|
||||
"echo '[]\n' > src/core/config/OperationConfig.json",
|
||||
"echo [] > src/core/config/OperationConfig.json",
|
||||
"node --experimental-modules --no-warnings --no-deprecation src/core/config/scripts/generateOpsIndex.mjs",
|
||||
"node --experimental-modules --no-warnings --no-deprecation src/core/config/scripts/generateConfig.mjs",
|
||||
"echo '--- Config scripts finished. ---\n'"
|
||||
@@ -403,7 +394,7 @@ module.exports = function (grunt) {
|
||||
command: "node --experimental-modules --no-warnings --no-deprecation tests/operations/index.mjs"
|
||||
},
|
||||
browserTests: {
|
||||
command: "./node_modules/.bin/nightwatch --env prod,inline"
|
||||
command: "./node_modules/.bin/nightwatch --env prod"
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -11,13 +11,22 @@ module.exports = function(api) {
|
||||
"node": "6.5"
|
||||
},
|
||||
"modules": false,
|
||||
"useBuiltIns": "entry"
|
||||
"useBuiltIns": "entry",
|
||||
"corejs": 3
|
||||
}]
|
||||
],
|
||||
"plugins": [
|
||||
["babel-plugin-transform-builtin-extend", {
|
||||
"globals": ["Error"]
|
||||
}]
|
||||
"babel-plugin-syntax-dynamic-import",
|
||||
[
|
||||
"babel-plugin-transform-builtin-extend", {
|
||||
"globals": ["Error"]
|
||||
}
|
||||
],
|
||||
[
|
||||
"@babel/plugin-transform-runtime", {
|
||||
"regenerator": true
|
||||
}
|
||||
]
|
||||
]
|
||||
};
|
||||
};
|
||||
|
||||
@@ -23,10 +23,6 @@
|
||||
|
||||
"prod": {
|
||||
"launch_url": "http://localhost:8000/index.html"
|
||||
},
|
||||
|
||||
"inline": {
|
||||
"launch_url": "http://localhost:8000/cyberchef.htm"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
7286
package-lock.json
generated
7286
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
114
package.json
114
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cyberchef",
|
||||
"version": "8.23.4",
|
||||
"version": "8.34.2",
|
||||
"description": "The Cyber Swiss Army Knife for encryption, encoding, compression and data analysis.",
|
||||
"author": "n1474335 <n1474335@gmail.com>",
|
||||
"homepage": "https://gchq.github.io/CyberChef",
|
||||
@@ -29,104 +29,122 @@
|
||||
},
|
||||
"main": "build/node/CyberChef.js",
|
||||
"bugs": "https://github.com/gchq/CyberChef/issues",
|
||||
"browserslist": [
|
||||
"Chrome >= 40",
|
||||
"Firefox >= 35",
|
||||
"Edge >= 14",
|
||||
"node >= 6.5"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.2.2",
|
||||
"@babel/preset-env": "^7.2.3",
|
||||
"autoprefixer": "^9.4.3",
|
||||
"babel-loader": "^8.0.4",
|
||||
"bootstrap": "^4.2.1",
|
||||
"chromedriver": "^2.45.0",
|
||||
"@babel/core": "^7.4.5",
|
||||
"@babel/plugin-transform-runtime": "^7.4.4",
|
||||
"@babel/preset-env": "^7.4.5",
|
||||
"autoprefixer": "^9.6.0",
|
||||
"babel-eslint": "^10.0.2",
|
||||
"babel-loader": "^8.0.6",
|
||||
"babel-plugin-syntax-dynamic-import": "^6.18.0",
|
||||
"chromedriver": "^75.0.0",
|
||||
"colors": "^1.3.3",
|
||||
"css-loader": "^2.1.0",
|
||||
"eslint": "^5.11.1",
|
||||
"css-loader": "^3.0.0",
|
||||
"eslint": "^6.0.1",
|
||||
"exports-loader": "^0.7.0",
|
||||
"extract-text-webpack-plugin": "^4.0.0-alpha0",
|
||||
"file-loader": "^3.0.1",
|
||||
"grunt": "^1.0.3",
|
||||
"file-loader": "^4.0.0",
|
||||
"grunt": "^1.0.4",
|
||||
"grunt-accessibility": "~6.0.0",
|
||||
"grunt-chmod": "~1.1.1",
|
||||
"grunt-concurrent": "^2.3.1",
|
||||
"grunt-concurrent": "^3.0.0",
|
||||
"grunt-contrib-clean": "~2.0.0",
|
||||
"grunt-contrib-connect": "^2.0.0",
|
||||
"grunt-contrib-copy": "~1.0.0",
|
||||
"grunt-contrib-watch": "^1.1.0",
|
||||
"grunt-eslint": "^21.0.0",
|
||||
"grunt-eslint": "^21.1.0",
|
||||
"grunt-exec": "~3.0.0",
|
||||
"grunt-jsdoc": "^2.3.0",
|
||||
"grunt-jsdoc": "^2.4.0",
|
||||
"grunt-webpack": "^3.1.3",
|
||||
"grunt-zip": "^0.18.2",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"imports-loader": "^0.8.0",
|
||||
"ink-docstrap": "^1.3.2",
|
||||
"jsdoc-babel": "^0.5.0",
|
||||
"nightwatch": "^1.0.18",
|
||||
"node-sass": "^4.11.0",
|
||||
"postcss-css-variables": "^0.11.0",
|
||||
"mini-css-extract-plugin": "^0.7.0",
|
||||
"nightwatch": "^1.1.12",
|
||||
"node-sass": "^4.12.0",
|
||||
"postcss-css-variables": "^0.13.0",
|
||||
"postcss-import": "^12.0.1",
|
||||
"postcss-loader": "^3.0.0",
|
||||
"prompt": "^1.0.0",
|
||||
"sass-loader": "^7.1.0",
|
||||
"sitemap": "^2.1.0",
|
||||
"sitemap": "^2.2.0",
|
||||
"style-loader": "^0.23.1",
|
||||
"url-loader": "^1.1.2",
|
||||
"web-resource-inliner": "^4.2.1",
|
||||
"webpack": "^4.28.3",
|
||||
"webpack-bundle-analyzer": "^3.0.3",
|
||||
"webpack-dev-server": "^3.1.14",
|
||||
"svg-url-loader": "^2.3.3",
|
||||
"url-loader": "^2.0.1",
|
||||
"webpack": "^4.35.0",
|
||||
"webpack-bundle-analyzer": "^3.3.2",
|
||||
"webpack-dev-server": "^3.7.2",
|
||||
"webpack-node-externals": "^1.7.2",
|
||||
"worker-loader": "^2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/polyfill": "^7.4.4",
|
||||
"@babel/runtime": "^7.4.5",
|
||||
"arrive": "^2.4.1",
|
||||
"babel-plugin-transform-builtin-extend": "1.1.2",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"bignumber.js": "^8.0.2",
|
||||
"bignumber.js": "^9.0.0",
|
||||
"blakejs": "^1.1.0",
|
||||
"bootstrap": "4.3.1",
|
||||
"bootstrap-colorpicker": "^2.5.3",
|
||||
"bootstrap-material-design": "^4.1.1",
|
||||
"bson": "^4.0.1",
|
||||
"bootstrap-material-design": "^4.1.2",
|
||||
"bson": "^4.0.2",
|
||||
"chi-squared": "^1.1.0",
|
||||
"clippyjs": "0.0.3",
|
||||
"core-js": "^3.1.4",
|
||||
"crypto-api": "^0.8.3",
|
||||
"crypto-js": "^3.1.9-1",
|
||||
"ctph.js": "0.0.5",
|
||||
"diff": "^3.5.0",
|
||||
"d3": "^5.9.4",
|
||||
"d3-hexbin": "^0.2.2",
|
||||
"diff": "^4.0.1",
|
||||
"es6-promisify": "^6.0.1",
|
||||
"escodegen": "^1.11.0",
|
||||
"escodegen": "^1.11.1",
|
||||
"esmangle": "^1.0.1",
|
||||
"esprima": "^4.0.1",
|
||||
"exif-parser": "^0.1.12",
|
||||
"file-saver": "^2.0.0",
|
||||
"file-saver": "^2.0.2",
|
||||
"geodesy": "^1.1.3",
|
||||
"highlight.js": "^9.13.1",
|
||||
"jimp": "^0.6.0",
|
||||
"jquery": "^3.3.1",
|
||||
"highlight.js": "^9.15.8",
|
||||
"jimp": "^0.6.4",
|
||||
"jquery": "3.4.1",
|
||||
"js-crc": "^0.2.0",
|
||||
"js-sha3": "^0.8.0",
|
||||
"jsesc": "^2.5.2",
|
||||
"jsonpath": "^1.0.0",
|
||||
"jsonwebtoken": "^8.4.0",
|
||||
"jsqr": "^1.1.1",
|
||||
"jsonpath": "^1.0.2",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"jsqr": "^1.2.0",
|
||||
"jsrsasign": "8.0.12",
|
||||
"kbpgp": "^2.0.82",
|
||||
"libyara-wasm": "0.0.11",
|
||||
"kbpgp": "2.1.2",
|
||||
"libbzip2-wasm": "0.0.4",
|
||||
"libyara-wasm": "0.0.12",
|
||||
"lodash": "^4.17.11",
|
||||
"loglevel": "^1.6.1",
|
||||
"loglevel": "^1.6.3",
|
||||
"loglevel-message-prefix": "^3.0.0",
|
||||
"moment": "^2.23.0",
|
||||
"moment-timezone": "^0.5.23",
|
||||
"moment": "^2.24.0",
|
||||
"moment-timezone": "^0.5.25",
|
||||
"ngeohash": "^0.6.3",
|
||||
"node-forge": "^0.7.6",
|
||||
"node-forge": "^0.8.5",
|
||||
"node-md6": "^0.1.0",
|
||||
"nodom": "^2.2.0",
|
||||
"notepack.io": "^2.2.0",
|
||||
"nwmatcher": "^1.4.4",
|
||||
"otp": "^0.1.3",
|
||||
"popper.js": "^1.14.6",
|
||||
"popper.js": "^1.15.0",
|
||||
"qr-image": "^3.2.0",
|
||||
"scryptsy": "^2.0.0",
|
||||
"scryptsy": "^2.1.0",
|
||||
"snackbarjs": "^1.1.0",
|
||||
"sortablejs": "^1.8.0-rc1",
|
||||
"split.js": "^1.5.10",
|
||||
"sortablejs": "^1.9.0",
|
||||
"split.js": "^1.5.11",
|
||||
"ssdeep.js": "0.0.2",
|
||||
"ua-parser-js": "^0.7.19",
|
||||
"ua-parser-js": "^0.7.20",
|
||||
"utf8": "^3.0.0",
|
||||
"vkbeautify": "^0.99.3",
|
||||
"xmldom": "^0.1.27",
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
module.exports = {
|
||||
plugins: [
|
||||
require("postcss-import"),
|
||||
require("autoprefixer")({
|
||||
browsers: [
|
||||
"Chrome >= 40",
|
||||
"Firefox >= 35",
|
||||
"Edge >= 14"
|
||||
]
|
||||
}),
|
||||
require("autoprefixer"),
|
||||
require("postcss-css-variables")({
|
||||
preserve: true
|
||||
}),
|
||||
|
||||
@@ -89,23 +89,26 @@ class Chef {
|
||||
progress = err.progress;
|
||||
}
|
||||
|
||||
// Depending on the size of the output, we may send it back as a string or an ArrayBuffer.
|
||||
// This can prevent unnecessary casting as an ArrayBuffer can be easily downloaded as a file.
|
||||
// The threshold is specified in KiB.
|
||||
const threshold = (options.ioDisplayThreshold || 1024) * 1024;
|
||||
const returnType = this.dish.size > threshold ? Dish.ARRAY_BUFFER : Dish.STRING;
|
||||
|
||||
// Create a raw version of the dish, unpresented
|
||||
const rawDish = this.dish.clone();
|
||||
|
||||
// Present the raw result
|
||||
await recipe.present(this.dish);
|
||||
|
||||
// Depending on the size of the output, we may send it back as a string or an ArrayBuffer.
|
||||
// This can prevent unnecessary casting as an ArrayBuffer can be easily downloaded as a file.
|
||||
// The threshold is specified in KiB.
|
||||
const threshold = (options.ioDisplayThreshold || 1024) * 1024;
|
||||
const returnType =
|
||||
this.dish.size > threshold ?
|
||||
Dish.ARRAY_BUFFER :
|
||||
this.dish.type === Dish.HTML ?
|
||||
Dish.HTML :
|
||||
Dish.STRING;
|
||||
|
||||
return {
|
||||
dish: rawDish,
|
||||
result: this.dish.type === Dish.HTML ?
|
||||
await this.dish.get(Dish.HTML, notUTF8) :
|
||||
await this.dish.get(returnType, notUTF8),
|
||||
result: await this.dish.get(returnType, notUTF8),
|
||||
type: Dish.enumLookup(this.dish.type),
|
||||
progress: progress,
|
||||
duration: new Date().getTime() - startTime,
|
||||
@@ -157,9 +160,9 @@ class Chef {
|
||||
* @param {number} pos.end - The end offset.
|
||||
* @returns {Object}
|
||||
*/
|
||||
calculateHighlights(recipeConfig, direction, pos) {
|
||||
async calculateHighlights(recipeConfig, direction, pos) {
|
||||
const recipe = new Recipe(recipeConfig);
|
||||
const highlights = recipe.generateHighlightList();
|
||||
const highlights = await recipe.generateHighlightList();
|
||||
|
||||
if (!highlights) return false;
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import "babel-polyfill";
|
||||
import Chef from "./Chef";
|
||||
import OperationConfig from "./config/OperationConfig.json";
|
||||
import OpModules from "./config/modules/OpModules";
|
||||
@@ -157,8 +156,8 @@ async function getDishAs(data) {
|
||||
* @param {number} pos.start - The start offset.
|
||||
* @param {number} pos.end - The end offset.
|
||||
*/
|
||||
function calculateHighlights(recipeConfig, direction, pos) {
|
||||
pos = self.chef.calculateHighlights(recipeConfig, direction, pos);
|
||||
async function calculateHighlights(recipeConfig, direction, pos) {
|
||||
pos = await self.chef.calculateHighlights(recipeConfig, direction, pos);
|
||||
|
||||
self.postMessage({
|
||||
action: "highlightsCalculated",
|
||||
@@ -179,7 +178,7 @@ self.loadRequiredModules = function(recipeConfig) {
|
||||
if (!OpModules.hasOwnProperty(module)) {
|
||||
log.info(`Loading ${module} module`);
|
||||
self.sendStatusMessage(`Loading ${module} module`);
|
||||
self.importScripts(`${self.docURL}/${module}.js`);
|
||||
self.importScripts(`${self.docURL}/modules/${module}.js`);
|
||||
self.sendStatusMessage("");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -21,8 +21,8 @@ class Dish {
|
||||
* @param {Dish} [dish=null] - A dish to clone
|
||||
*/
|
||||
constructor(dish=null) {
|
||||
this.value = [];
|
||||
this.type = Dish.BYTE_ARRAY;
|
||||
this.value = new ArrayBuffer(0);
|
||||
this.type = Dish.ARRAY_BUFFER;
|
||||
|
||||
if (dish &&
|
||||
dish.hasOwnProperty("value") &&
|
||||
@@ -149,78 +149,75 @@ class Dish {
|
||||
*/
|
||||
async _translate(toType, notUTF8=false) {
|
||||
log.debug(`Translating Dish from ${Dish.enumLookup(this.type)} to ${Dish.enumLookup(toType)}`);
|
||||
const byteArrayToStr = notUTF8 ? Utils.byteArrayToChars : Utils.byteArrayToUtf8;
|
||||
|
||||
// Convert data to intermediate byteArray type
|
||||
// Convert data to intermediate ArrayBuffer type
|
||||
try {
|
||||
switch (this.type) {
|
||||
case Dish.STRING:
|
||||
this.value = this.value ? Utils.strToByteArray(this.value) : [];
|
||||
this.value = this.value ? Utils.strToArrayBuffer(this.value) : new ArrayBuffer;
|
||||
break;
|
||||
case Dish.NUMBER:
|
||||
this.value = typeof this.value === "number" ? Utils.strToByteArray(this.value.toString()) : [];
|
||||
this.value = typeof this.value === "number" ? Utils.strToArrayBuffer(this.value.toString()) : new ArrayBuffer;
|
||||
break;
|
||||
case Dish.HTML:
|
||||
this.value = this.value ? Utils.strToByteArray(Utils.unescapeHtml(Utils.stripHtmlTags(this.value, true))) : [];
|
||||
this.value = this.value ? Utils.strToArrayBuffer(Utils.unescapeHtml(Utils.stripHtmlTags(this.value, true))) : new ArrayBuffer;
|
||||
break;
|
||||
case Dish.ARRAY_BUFFER:
|
||||
// Array.from() would be nicer here, but it's slightly slower
|
||||
this.value = Array.prototype.slice.call(new Uint8Array(this.value));
|
||||
case Dish.BYTE_ARRAY:
|
||||
this.value = new Uint8Array(this.value).buffer;
|
||||
break;
|
||||
case Dish.BIG_NUMBER:
|
||||
this.value = BigNumber.isBigNumber(this.value) ? Utils.strToByteArray(this.value.toFixed()) : [];
|
||||
this.value = BigNumber.isBigNumber(this.value) ? Utils.strToArrayBuffer(this.value.toFixed()) : new ArrayBuffer;
|
||||
break;
|
||||
case Dish.JSON:
|
||||
this.value = this.value ? Utils.strToByteArray(JSON.stringify(this.value, null, 4)) : [];
|
||||
this.value = this.value ? Utils.strToArrayBuffer(JSON.stringify(this.value, null, 4)) : new ArrayBuffer;
|
||||
break;
|
||||
case Dish.FILE:
|
||||
this.value = await Utils.readFile(this.value);
|
||||
this.value = Array.prototype.slice.call(this.value);
|
||||
this.value = (await Utils.readFile(this.value)).buffer;
|
||||
break;
|
||||
case Dish.LIST_FILE:
|
||||
this.value = await Promise.all(this.value.map(async f => Utils.readFile(f)));
|
||||
this.value = this.value.map(b => Array.prototype.slice.call(b));
|
||||
this.value = [].concat.apply([], this.value);
|
||||
this.value = concatenateTypedArrays(...this.value).buffer;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
throw new DishError(`Error translating from ${Dish.enumLookup(this.type)} to byteArray: ${err}`);
|
||||
throw new DishError(`Error translating from ${Dish.enumLookup(this.type)} to ArrayBuffer: ${err}`);
|
||||
}
|
||||
|
||||
this.type = Dish.BYTE_ARRAY;
|
||||
this.type = Dish.ARRAY_BUFFER;
|
||||
|
||||
// Convert from byteArray to toType
|
||||
// Convert from ArrayBuffer to toType
|
||||
try {
|
||||
switch (toType) {
|
||||
case Dish.STRING:
|
||||
case Dish.HTML:
|
||||
this.value = this.value ? byteArrayToStr(this.value) : "";
|
||||
this.value = this.value ? Utils.arrayBufferToStr(this.value, !notUTF8) : "";
|
||||
this.type = Dish.STRING;
|
||||
break;
|
||||
case Dish.NUMBER:
|
||||
this.value = this.value ? parseFloat(byteArrayToStr(this.value)) : 0;
|
||||
this.value = this.value ? parseFloat(Utils.arrayBufferToStr(this.value, !notUTF8)) : 0;
|
||||
this.type = Dish.NUMBER;
|
||||
break;
|
||||
case Dish.ARRAY_BUFFER:
|
||||
this.value = new Uint8Array(this.value).buffer;
|
||||
case Dish.BYTE_ARRAY:
|
||||
this.value = Array.prototype.slice.call(new Uint8Array(this.value));
|
||||
this.type = Dish.ARRAY_BUFFER;
|
||||
break;
|
||||
case Dish.BIG_NUMBER:
|
||||
try {
|
||||
this.value = new BigNumber(byteArrayToStr(this.value));
|
||||
this.value = new BigNumber(Utils.arrayBufferToStr(this.value, !notUTF8));
|
||||
} catch (err) {
|
||||
this.value = new BigNumber(NaN);
|
||||
}
|
||||
this.type = Dish.BIG_NUMBER;
|
||||
break;
|
||||
case Dish.JSON:
|
||||
this.value = JSON.parse(byteArrayToStr(this.value));
|
||||
this.value = JSON.parse(Utils.arrayBufferToStr(this.value, !notUTF8));
|
||||
this.type = Dish.JSON;
|
||||
break;
|
||||
case Dish.FILE:
|
||||
this.value = new File(this.value, "unknown");
|
||||
this.type = Dish.FILE;
|
||||
break;
|
||||
case Dish.LIST_FILE:
|
||||
this.value = [new File(this.value, "unknown")];
|
||||
@@ -230,7 +227,7 @@ class Dish {
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
throw new DishError(`Error translating from byteArray to ${Dish.enumLookup(toType)}: ${err}`);
|
||||
throw new DishError(`Error translating from ArrayBuffer to ${Dish.enumLookup(toType)}: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,6 +371,26 @@ class Dish {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Concatenates a list of Uint8Arrays together
|
||||
*
|
||||
* @param {Uint8Array[]} arrays
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
function concatenateTypedArrays(...arrays) {
|
||||
let totalLength = 0;
|
||||
for (const arr of arrays) {
|
||||
totalLength += arr.length;
|
||||
}
|
||||
const result = new Uint8Array(totalLength);
|
||||
let offset = 0;
|
||||
for (const arr of arrays) {
|
||||
result.set(arr, offset);
|
||||
offset += arr.length;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Dish data type enum for byte arrays.
|
||||
|
||||
@@ -27,6 +27,9 @@ class Ingredient {
|
||||
this.toggleValues = [];
|
||||
this.target = null;
|
||||
this.defaultIndex = 0;
|
||||
this.min = null;
|
||||
this.max = null;
|
||||
this.step = 1;
|
||||
|
||||
if (ingredientConfig) {
|
||||
this._parseConfig(ingredientConfig);
|
||||
@@ -50,6 +53,9 @@ class Ingredient {
|
||||
this.toggleValues = ingredientConfig.toggleValues;
|
||||
this.target = typeof ingredientConfig.target !== "undefined" ? ingredientConfig.target : null;
|
||||
this.defaultIndex = typeof ingredientConfig.defaultIndex !== "undefined" ? ingredientConfig.defaultIndex : 0;
|
||||
this.min = ingredientConfig.min;
|
||||
this.max = ingredientConfig.max;
|
||||
this.step = ingredientConfig.step;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ class Operation {
|
||||
this._breakpoint = false;
|
||||
this._disabled = false;
|
||||
this._flowControl = false;
|
||||
this._manualBake = false;
|
||||
this._ingList = [];
|
||||
|
||||
// Public fields
|
||||
@@ -183,6 +184,9 @@ class Operation {
|
||||
if (ing.disabled) conf.disabled = ing.disabled;
|
||||
if (ing.target) conf.target = ing.target;
|
||||
if (ing.defaultIndex) conf.defaultIndex = ing.defaultIndex;
|
||||
if (typeof ing.min === "number") conf.min = ing.min;
|
||||
if (typeof ing.max === "number") conf.max = ing.max;
|
||||
if (ing.step) conf.step = ing.step;
|
||||
return conf;
|
||||
});
|
||||
}
|
||||
@@ -282,6 +286,7 @@ class Operation {
|
||||
return this._flowControl;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set whether this Operation is a flowcontrol op.
|
||||
*
|
||||
@@ -291,6 +296,26 @@ class Operation {
|
||||
this._flowControl = !!value;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns true if this Operation should not trigger AutoBake.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get manualBake() {
|
||||
return this._manualBake;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set whether this Operation should trigger AutoBake.
|
||||
*
|
||||
* @param {boolean} value
|
||||
*/
|
||||
set manualBake(value) {
|
||||
this._manualBake = !!value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default Operation;
|
||||
|
||||
@@ -4,12 +4,15 @@
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import OpModules from "./config/modules/OpModules";
|
||||
import OperationConfig from "./config/OperationConfig.json";
|
||||
import OperationError from "./errors/OperationError";
|
||||
import Operation from "./Operation";
|
||||
import DishError from "./errors/DishError";
|
||||
import log from "loglevel";
|
||||
|
||||
// Cache container for modules
|
||||
let modules = null;
|
||||
|
||||
/**
|
||||
* The Recipe controls a list of Operations and the Dish they operate on.
|
||||
*/
|
||||
@@ -36,16 +39,43 @@ class Recipe {
|
||||
* @param {Object} recipeConfig
|
||||
*/
|
||||
_parseConfig(recipeConfig) {
|
||||
for (let c = 0; c < recipeConfig.length; c++) {
|
||||
const operationName = recipeConfig[c].op;
|
||||
const opConf = OperationConfig[operationName];
|
||||
const opObj = OpModules[opConf.module][operationName];
|
||||
const operation = new opObj();
|
||||
operation.ingValues = recipeConfig[c].args;
|
||||
operation.breakpoint = recipeConfig[c].breakpoint;
|
||||
operation.disabled = recipeConfig[c].disabled;
|
||||
this.addOperation(operation);
|
||||
recipeConfig.forEach(c => {
|
||||
this.opList.push({
|
||||
name: c.op,
|
||||
module: OperationConfig[c.op].module,
|
||||
ingValues: c.args,
|
||||
breakpoint: c.breakpoint,
|
||||
disabled: c.disabled,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Populate elements of opList with operation instances.
|
||||
* Dynamic import here removes top-level cyclic dependency issue.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
async _hydrateOpList() {
|
||||
if (!modules) {
|
||||
// Using Webpack Magic Comments to force the dynamic import to be included in the main chunk
|
||||
// https://webpack.js.org/api/module-methods/
|
||||
modules = await import(/* webpackMode: "eager" */ "./config/modules/OpModules");
|
||||
modules = modules.default;
|
||||
}
|
||||
|
||||
this.opList = this.opList.map(o => {
|
||||
if (o instanceof Operation) {
|
||||
return o;
|
||||
} else {
|
||||
const op = new modules[o.module][o.name]();
|
||||
op.ingValues = o.ingValues;
|
||||
op.breakpoint = o.breakpoint;
|
||||
op.disabled = o.disabled;
|
||||
return op;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +85,10 @@ class Recipe {
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
get config() {
|
||||
return this.opList.map(op => op.config);
|
||||
return this.opList.map(op => ({
|
||||
op: op.name,
|
||||
args: op.ingValues,
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +108,19 @@ class Recipe {
|
||||
* @param {Operation[]} operations
|
||||
*/
|
||||
addOperations(operations) {
|
||||
this.opList = this.opList.concat(operations);
|
||||
operations.forEach(o => {
|
||||
if (o instanceof Operation) {
|
||||
this.opList.push(o);
|
||||
} else {
|
||||
this.opList.push({
|
||||
name: o.name,
|
||||
module: o.module,
|
||||
ingValues: o.args,
|
||||
breakpoint: o.breakpoint,
|
||||
disabled: o.disabled,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -108,7 +153,7 @@ class Recipe {
|
||||
|
||||
|
||||
/**
|
||||
* Returns true if there is an Flow Control Operation in this Recipe.
|
||||
* Returns true if there is a Flow Control Operation in this Recipe.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
@@ -137,6 +182,8 @@ class Recipe {
|
||||
|
||||
if (startFrom === 0) this.lastRunOp = null;
|
||||
|
||||
await this._hydrateOpList();
|
||||
|
||||
log.debug(`[*] Executing recipe of ${this.opList.length} operations, starting at ${startFrom}`);
|
||||
|
||||
for (let i = startFrom; i < this.opList.length; i++) {
|
||||
@@ -254,7 +301,8 @@ class Recipe {
|
||||
* @returns {function} highlights[].b
|
||||
* @returns {Object[]} highlights[].args
|
||||
*/
|
||||
generateHighlightList() {
|
||||
async generateHighlightList() {
|
||||
await this._hydrateOpList();
|
||||
const highlights = [];
|
||||
|
||||
for (let i = 0; i < this.opList.length; i++) {
|
||||
|
||||
@@ -201,11 +201,20 @@ class Utils {
|
||||
* Utils.parseEscapedChars("\\n");
|
||||
*/
|
||||
static parseEscapedChars(str) {
|
||||
return str.replace(/(\\)?\\([bfnrtv0'"]|x[\da-fA-F]{2}|u[\da-fA-F]{4}|u\{[\da-fA-F]{1,6}\})/g, function(m, a, b) {
|
||||
return str.replace(/(\\)?\\([bfnrtv'"]|[0-3][0-7]{2}|[0-7]{1,2}|x[\da-fA-F]{2}|u[\da-fA-F]{4}|u\{[\da-fA-F]{1,6}\}|\\)/g, function(m, a, b) {
|
||||
if (a === "\\") return "\\"+b;
|
||||
switch (b[0]) {
|
||||
case "\\":
|
||||
return "\\";
|
||||
case "0":
|
||||
return "\0";
|
||||
case "1":
|
||||
case "2":
|
||||
case "3":
|
||||
case "4":
|
||||
case "5":
|
||||
case "6":
|
||||
case "7":
|
||||
return String.fromCharCode(parseInt(b, 8));
|
||||
case "b":
|
||||
return "\b";
|
||||
case "t":
|
||||
@@ -367,6 +376,61 @@ class Utils {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Converts a string to an ArrayBuffer.
|
||||
* Treats the string as UTF-8 if any values are over 255.
|
||||
*
|
||||
* @param {string} str
|
||||
* @returns {ArrayBuffer}
|
||||
*
|
||||
* @example
|
||||
* // returns [72,101,108,108,111]
|
||||
* Utils.strToArrayBuffer("Hello");
|
||||
*
|
||||
* // returns [228,189,160,229,165,189]
|
||||
* Utils.strToArrayBuffer("你好");
|
||||
*/
|
||||
static strToArrayBuffer(str) {
|
||||
const arr = new Uint8Array(str.length);
|
||||
let i = str.length, b;
|
||||
while (i--) {
|
||||
b = str.charCodeAt(i);
|
||||
arr[i] = b;
|
||||
// If any of the bytes are over 255, read as UTF-8
|
||||
if (b > 255) return Utils.strToUtf8ArrayBuffer(str);
|
||||
}
|
||||
return arr.buffer;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Converts a string to a UTF-8 ArrayBuffer.
|
||||
*
|
||||
* @param {string} str
|
||||
* @returns {ArrayBuffer}
|
||||
*
|
||||
* @example
|
||||
* // returns [72,101,108,108,111]
|
||||
* Utils.strToUtf8ArrayBuffer("Hello");
|
||||
*
|
||||
* // returns [228,189,160,229,165,189]
|
||||
* Utils.strToUtf8ArrayBuffer("你好");
|
||||
*/
|
||||
static strToUtf8ArrayBuffer(str) {
|
||||
const utf8Str = utf8.encode(str);
|
||||
|
||||
if (str.length !== utf8Str.length) {
|
||||
if (ENVIRONMENT_IS_WORKER()) {
|
||||
self.setOption("attemptHighlight", false);
|
||||
} else if (ENVIRONMENT_IS_WEB()) {
|
||||
window.app.options.attemptHighlight = false;
|
||||
}
|
||||
}
|
||||
|
||||
return Utils.strToArrayBuffer(utf8Str);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Converts a string to a byte array.
|
||||
* Treats the string as UTF-8 if any values are over 255.
|
||||
@@ -459,7 +523,7 @@ class Utils {
|
||||
/**
|
||||
* Attempts to convert a byte array to a UTF-8 string.
|
||||
*
|
||||
* @param {byteArray} byteArray
|
||||
* @param {byteArray|Uint8Array} byteArray
|
||||
* @returns {string}
|
||||
*
|
||||
* @example
|
||||
@@ -505,6 +569,7 @@ class Utils {
|
||||
static byteArrayToChars(byteArray) {
|
||||
if (!byteArray) return "";
|
||||
let str = "";
|
||||
// String concatenation appears to be faster than an array join
|
||||
for (let i = 0; i < byteArray.length;) {
|
||||
str += String.fromCharCode(byteArray[i++]);
|
||||
}
|
||||
@@ -524,8 +589,8 @@ class Utils {
|
||||
* Utils.arrayBufferToStr(Uint8Array.from([104,101,108,108,111]).buffer);
|
||||
*/
|
||||
static arrayBufferToStr(arrayBuffer, utf8=true) {
|
||||
const byteArray = Array.prototype.slice.call(new Uint8Array(arrayBuffer));
|
||||
return utf8 ? Utils.byteArrayToUtf8(byteArray) : Utils.byteArrayToChars(byteArray);
|
||||
const arr = new Uint8Array(arrayBuffer);
|
||||
return utf8 ? Utils.byteArrayToUtf8(arr) : Utils.byteArrayToChars(arr);
|
||||
}
|
||||
|
||||
|
||||
@@ -780,7 +845,7 @@ class Utils {
|
||||
args = m[2]
|
||||
.replace(/"/g, '\\"') // Escape double quotes
|
||||
.replace(/(^|,|{|:)'/g, '$1"') // Replace opening ' with "
|
||||
.replace(/([^\\]|[^\\]\\\\)'(,|:|}|$)/g, '$1"$2') // Replace closing ' with "
|
||||
.replace(/([^\\]|(?:\\\\)+)'(,|:|}|$)/g, '$1"$2') // Replace closing ' with "
|
||||
.replace(/\\'/g, "'"); // Unescape single quotes
|
||||
args = "[" + args + "]";
|
||||
|
||||
@@ -832,8 +897,9 @@ class Utils {
|
||||
const buff = await Utils.readFile(file);
|
||||
const blob = new Blob(
|
||||
[buff],
|
||||
{type: "octet/stream"}
|
||||
{type: file.type || "octet/stream"}
|
||||
);
|
||||
const blobURL = URL.createObjectURL(blob);
|
||||
|
||||
const html = `<div class='card' style='white-space: normal;'>
|
||||
<div class='card-header' id='heading${i}'>
|
||||
@@ -848,10 +914,19 @@ class Utils {
|
||||
<span class='float-right' style="margin-top: -3px">
|
||||
${file.size.toLocaleString()} bytes
|
||||
<a title="Download ${Utils.escapeHtml(file.name)}"
|
||||
href='${URL.createObjectURL(blob)}'
|
||||
download='${Utils.escapeHtml(file.name)}'>
|
||||
href="${blobURL}"
|
||||
download="${Utils.escapeHtml(file.name)}"
|
||||
data-toggle="tooltip">
|
||||
<i class="material-icons" style="vertical-align: bottom">save</i>
|
||||
</a>
|
||||
<a title="Move to input"
|
||||
href="#"
|
||||
blob-url="${blobURL}"
|
||||
file-name="${Utils.escapeHtml(file.name)}"
|
||||
class="extract-file"
|
||||
data-toggle="tooltip">
|
||||
<i class="material-icons" style="vertical-align: bottom">open_in_browser</i>
|
||||
</a>
|
||||
</span>
|
||||
</h6>
|
||||
</div>
|
||||
@@ -1013,9 +1088,11 @@ class Utils {
|
||||
static charRep(token) {
|
||||
return {
|
||||
"Space": " ",
|
||||
"Percent": "%",
|
||||
"Comma": ",",
|
||||
"Semi-colon": ";",
|
||||
"Colon": ":",
|
||||
"Tab": "\t",
|
||||
"Line feed": "\n",
|
||||
"CRLF": "\r\n",
|
||||
"Forward slash": "/",
|
||||
@@ -1037,6 +1114,7 @@ class Utils {
|
||||
static regexRep(token) {
|
||||
return {
|
||||
"Space": /\s+/g,
|
||||
"Percent": /%/g,
|
||||
"Comma": /,/g,
|
||||
"Semi-colon": /;/g,
|
||||
"Colon": /:/g,
|
||||
@@ -1163,6 +1241,21 @@ String.prototype.count = function(chr) {
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Wrapper for self.sendStatusMessage to handle different environments.
|
||||
*
|
||||
* @param {string} msg
|
||||
*/
|
||||
export function sendStatusMessage(msg) {
|
||||
if (ENVIRONMENT_IS_WORKER())
|
||||
self.sendStatusMessage(msg);
|
||||
else if (ENVIRONMENT_IS_WEB())
|
||||
app.alert(msg, 10000);
|
||||
else if (ENVIRONMENT_IS_NODE())
|
||||
log.debug(msg);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Polyfills
|
||||
*/
|
||||
|
||||
@@ -102,7 +102,11 @@
|
||||
"JWT Decode",
|
||||
"Citrix CTX1 Encode",
|
||||
"Citrix CTX1 Decode",
|
||||
"Pseudo-Random Number Generator"
|
||||
"Pseudo-Random Number Generator",
|
||||
"Enigma",
|
||||
"Bombe",
|
||||
"Multiple Bombe",
|
||||
"Typex"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -155,6 +159,7 @@
|
||||
"name": "Networking",
|
||||
"ops": [
|
||||
"HTTP request",
|
||||
"DNS over HTTPS",
|
||||
"Strip HTTP headers",
|
||||
"Dechunk HTTP response",
|
||||
"Parse User Agent",
|
||||
@@ -164,6 +169,9 @@
|
||||
"Parse URI",
|
||||
"URL Encode",
|
||||
"URL Decode",
|
||||
"Protobuf Decode",
|
||||
"VarInt Encode",
|
||||
"VarInt Decode",
|
||||
"Format MAC addresses",
|
||||
"Change IP format",
|
||||
"Group IP addresses",
|
||||
@@ -253,7 +261,8 @@
|
||||
"XPath expression",
|
||||
"JPath expression",
|
||||
"CSS selector",
|
||||
"Extract EXIF"
|
||||
"Extract EXIF",
|
||||
"Extract Files"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -291,6 +300,8 @@
|
||||
"HAS-160",
|
||||
"Whirlpool",
|
||||
"Snefru",
|
||||
"BLAKE2b",
|
||||
"BLAKE2s",
|
||||
"SSDEEP",
|
||||
"CTPH",
|
||||
"Compare SSDEEP hashes",
|
||||
@@ -347,6 +358,7 @@
|
||||
"ops": [
|
||||
"Detect File Type",
|
||||
"Scan for Embedded Files",
|
||||
"Extract Files",
|
||||
"Remove EXIF",
|
||||
"Extract EXIF"
|
||||
]
|
||||
@@ -358,7 +370,24 @@
|
||||
"Play Media",
|
||||
"Remove EXIF",
|
||||
"Extract EXIF",
|
||||
"Split Colour Channels"
|
||||
"Split Colour Channels",
|
||||
"Rotate Image",
|
||||
"Resize Image",
|
||||
"Blur Image",
|
||||
"Dither Image",
|
||||
"Invert Image",
|
||||
"Flip Image",
|
||||
"Crop Image",
|
||||
"Image Brightness / Contrast",
|
||||
"Image Opacity",
|
||||
"Image Filter",
|
||||
"Contain Image",
|
||||
"Cover Image",
|
||||
"Image Hue/Saturation/Lightness",
|
||||
"Hex Density chart",
|
||||
"Scatter chart",
|
||||
"Series chart",
|
||||
"Heatmap chart"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -366,6 +395,7 @@
|
||||
"ops": [
|
||||
"Entropy",
|
||||
"Frequency distribution",
|
||||
"Index of Coincidence",
|
||||
"Chi Square",
|
||||
"Disassemble x86",
|
||||
"Pseudo-Random Number Generator",
|
||||
@@ -375,6 +405,7 @@
|
||||
"Generate QR Code",
|
||||
"Parse QR Code",
|
||||
"Haversine distance",
|
||||
"HTML To Text",
|
||||
"Generate Lorem Ipsum",
|
||||
"Numberwang",
|
||||
"XKCD Random Number"
|
||||
|
||||
@@ -41,6 +41,7 @@ for (const opObj in Ops) {
|
||||
inputType: op.inputType,
|
||||
outputType: op.presentType,
|
||||
flowControl: op.flowControl,
|
||||
manualBake: op.manualBake,
|
||||
args: op.args
|
||||
};
|
||||
|
||||
|
||||
0
src/core/lib/BCD.mjs
Executable file → Normal file
0
src/core/lib/BCD.mjs
Executable file → Normal file
0
src/core/lib/Base58.mjs
Executable file → Normal file
0
src/core/lib/Base58.mjs
Executable file → Normal file
0
src/core/lib/Base64.mjs
Executable file → Normal file
0
src/core/lib/Base64.mjs
Executable file → Normal file
756
src/core/lib/Bombe.mjs
Normal file
756
src/core/lib/Bombe.mjs
Normal file
@@ -0,0 +1,756 @@
|
||||
/**
|
||||
* Emulation of the Bombe machine.
|
||||
*
|
||||
* @author s2224834
|
||||
* @author The National Museum of Computing - Bombe Rebuild Project
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import OperationError from "../errors/OperationError";
|
||||
import Utils from "../Utils";
|
||||
import {Rotor, Plugboard, a2i, i2a} from "./Enigma";
|
||||
|
||||
/**
|
||||
* Convenience/optimisation subclass of Rotor
|
||||
*
|
||||
* This allows creating multiple Rotors which share backing maps, to avoid repeatedly parsing the
|
||||
* rotor spec strings and duplicating the maps in memory.
|
||||
*/
|
||||
class CopyRotor extends Rotor {
|
||||
/**
|
||||
* Return a copy of this Rotor.
|
||||
* @returns {Object}
|
||||
*/
|
||||
copy() {
|
||||
const clone = {
|
||||
map: this.map,
|
||||
revMap: this.revMap,
|
||||
pos: this.pos,
|
||||
step: this.step,
|
||||
transform: this.transform,
|
||||
revTransform: this.revTransform,
|
||||
};
|
||||
return clone;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Node in the menu graph
|
||||
*
|
||||
* A node represents a cipher/plaintext letter.
|
||||
*/
|
||||
class Node {
|
||||
/**
|
||||
* Node constructor.
|
||||
* @param {number} letter - The plain/ciphertext letter this node represents (as a number).
|
||||
*/
|
||||
constructor(letter) {
|
||||
this.letter = letter;
|
||||
this.edges = new Set();
|
||||
this.visited = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Edge in the menu graph
|
||||
*
|
||||
* An edge represents an Enigma machine transformation between two letters.
|
||||
*/
|
||||
class Edge {
|
||||
/**
|
||||
* Edge constructor - an Enigma machine mapping between letters
|
||||
* @param {number} pos - The rotor position, relative to the beginning of the crib, at this edge
|
||||
* @param {number} node1 - Letter at one end (as a number)
|
||||
* @param {number} node2 - Letter at the other end
|
||||
*/
|
||||
constructor(pos, node1, node2) {
|
||||
this.pos = pos;
|
||||
this.node1 = node1;
|
||||
this.node2 = node2;
|
||||
node1.edges.add(this);
|
||||
node2.edges.add(this);
|
||||
this.visited = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the node at one end of this edge, return the other end.
|
||||
* @param node {number} - The node we have
|
||||
* @returns {number}
|
||||
*/
|
||||
getOther(node) {
|
||||
return this.node1 === node ? this.node2 : this.node1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* As all the Bombe's rotors move in step, at any given point the vast majority of the scramblers
|
||||
* in the machine share the majority of their state, which is hosted in this class.
|
||||
*/
|
||||
class SharedScrambler {
|
||||
/**
|
||||
* SharedScrambler constructor.
|
||||
* @param {Object[]} rotors - List of rotors in the shared state _only_.
|
||||
* @param {Object} reflector - The reflector in use.
|
||||
*/
|
||||
constructor(rotors, reflector) {
|
||||
this.lowerCache = new Array(26);
|
||||
this.higherCache = new Array(26);
|
||||
for (let i=0; i<26; i++) {
|
||||
this.higherCache[i] = new Array(26);
|
||||
}
|
||||
this.changeRotors(rotors, reflector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the rotors and reflector in this SharedScrambler.
|
||||
* This takes care of flushing caches as well.
|
||||
* @param {Object[]} rotors - List of rotors in the shared state _only_.
|
||||
* @param {Object} reflector - The reflector in use.
|
||||
*/
|
||||
changeRotors(rotors, reflector) {
|
||||
this.reflector = reflector;
|
||||
this.rotors = rotors;
|
||||
this.rotorsRev = [].concat(rotors).reverse();
|
||||
this.cacheGen();
|
||||
}
|
||||
|
||||
/**
|
||||
* Step the rotors forward.
|
||||
* @param {number} n - How many rotors to step. This includes the rotors which are not part of
|
||||
* the shared state, so should be 2 or more.
|
||||
*/
|
||||
step(n) {
|
||||
for (let i=0; i<n-1; i++) {
|
||||
this.rotors[i].step();
|
||||
}
|
||||
this.cacheGen();
|
||||
}
|
||||
|
||||
/**
|
||||
* Optimisation: We pregenerate all routes through the machine with the top rotor removed,
|
||||
* as these rarely change. This saves a lot of lookups. This function generates this route
|
||||
* table.
|
||||
* We also just-in-time cache the full routes through the scramblers, because after stepping
|
||||
* the fast rotor some scramblers will be in states occupied by other scrambles on previous
|
||||
* iterations.
|
||||
*/
|
||||
cacheGen() {
|
||||
for (let i=0; i<26; i++) {
|
||||
this.lowerCache[i] = undefined;
|
||||
for (let j=0; j<26; j++) {
|
||||
this.higherCache[i][j] = undefined;
|
||||
}
|
||||
}
|
||||
for (let i=0; i<26; i++) {
|
||||
if (this.lowerCache[i] !== undefined) {
|
||||
continue;
|
||||
}
|
||||
let letter = i;
|
||||
for (const rotor of this.rotors) {
|
||||
letter = rotor.transform(letter);
|
||||
}
|
||||
letter = this.reflector.transform(letter);
|
||||
for (const rotor of this.rotorsRev) {
|
||||
letter = rotor.revTransform(letter);
|
||||
}
|
||||
// By symmetry
|
||||
this.lowerCache[i] = letter;
|
||||
this.lowerCache[letter] = i;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a letter through this (partial) scrambler.
|
||||
* @param {number} i - The letter
|
||||
* @returns {number}
|
||||
*/
|
||||
transform(i) {
|
||||
return this.lowerCache[i];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrambler.
|
||||
*
|
||||
* This is effectively just an Enigma machine, but it only operates on one character at a time and
|
||||
* the stepping mechanism is different.
|
||||
*/
|
||||
class Scrambler {
|
||||
/** Scrambler constructor.
|
||||
* @param {Object} base - The SharedScrambler whose state this scrambler uses
|
||||
* @param {Object} rotor - The non-shared fast rotor in this scrambler
|
||||
* @param {number} pos - Position offset from start of crib
|
||||
* @param {number} end1 - Letter in menu this scrambler is attached to
|
||||
* @param {number} end2 - Other letter in menu this scrambler is attached to
|
||||
*/
|
||||
constructor(base, rotor, pos, end1, end2) {
|
||||
this.baseScrambler = base;
|
||||
this.initialPos = pos;
|
||||
this.changeRotor(rotor);
|
||||
this.end1 = end1;
|
||||
this.end2 = end2;
|
||||
// For efficiency reasons, we pull the relevant shared cache from the baseScrambler into
|
||||
// this object - this saves us a few pointer dereferences
|
||||
this.cache = this.baseScrambler.higherCache[pos];
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the rotor in this scrambler.
|
||||
* The position is reset automatically.
|
||||
* @param {Object} rotor - New rotor
|
||||
*/
|
||||
changeRotor(rotor) {
|
||||
this.rotor = rotor;
|
||||
this.rotor.pos += this.initialPos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Step the rotor forward.
|
||||
*
|
||||
* The base SharedScrambler needs to be instructed to step separately.
|
||||
*/
|
||||
step() {
|
||||
// The Bombe steps the slowest rotor on an actual Enigma fastest, for reasons.
|
||||
// ...but for optimisation reasons I'm going to cheat and not do that, as this vastly
|
||||
// simplifies caching the state of the majority of the scramblers. The results are the
|
||||
// same, just in a slightly different order.
|
||||
this.rotor.step();
|
||||
this.cache = this.baseScrambler.higherCache[this.rotor.pos];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Run a letter through the scrambler.
|
||||
* @param {number} i - The letter to transform (as a number)
|
||||
* @returns {number}
|
||||
*/
|
||||
transform(i) {
|
||||
let letter = i;
|
||||
const cached = this.cache[i];
|
||||
if (cached !== undefined) {
|
||||
return cached;
|
||||
}
|
||||
letter = this.rotor.transform(letter);
|
||||
letter = this.baseScrambler.transform(letter);
|
||||
letter = this.rotor.revTransform(letter);
|
||||
this.cache[i] = letter;
|
||||
this.cache[letter] = i;
|
||||
return letter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given one letter in the menu this scrambler maps to, return the other.
|
||||
* @param end {number} - The node we have
|
||||
* @returns {number}
|
||||
*/
|
||||
getOtherEnd(end) {
|
||||
return this.end1 === end ? this.end2 : this.end1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the position this scrambler is set to.
|
||||
* Note that because of Enigma's stepping, you need to set an actual Enigma to the previous
|
||||
* position in order to get it to make a certain set of electrical connections when a button
|
||||
* is pressed - this function *does* take this into account.
|
||||
* However, as with the rest of the Bombe, it does not take stepping into account - the middle
|
||||
* and slow rotors are treated as static.
|
||||
* @return {string}
|
||||
*/
|
||||
getPos() {
|
||||
let result = "";
|
||||
// Roll back the fast rotor by one step
|
||||
let pos = Utils.mod(this.rotor.pos - 1, 26);
|
||||
result += i2a(pos);
|
||||
for (let i=0; i<this.baseScrambler.rotors.length; i++) {
|
||||
pos = this.baseScrambler.rotors[i].pos;
|
||||
result += i2a(pos);
|
||||
}
|
||||
return result.split("").reverse().join("");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Bombe simulator class.
|
||||
*/
|
||||
export class BombeMachine {
|
||||
/**
|
||||
* Construct a Bombe.
|
||||
*
|
||||
* Note that there is no handling of offsets here: the crib specified must exactly match the
|
||||
* ciphertext. It will check that the crib is sane (length is vaguely sensible and there's no
|
||||
* matching characters between crib and ciphertext) but cannot check further - if it's wrong
|
||||
* your results will be wrong!
|
||||
*
|
||||
* There is also no handling of rotor stepping - if the target Enigma stepped in the middle of
|
||||
* your crib, you're out of luck. TODO: Allow specifying a step point - this is fairly easy to
|
||||
* configure on a real Bombe, but we're not clear on whether it was ever actually done for
|
||||
* real (there would almost certainly have been better ways of attacking in most situations
|
||||
* than attempting to exhaust options for the stepping point, but in some circumstances, e.g.
|
||||
* via Banburismus, the stepping point might have been known).
|
||||
*
|
||||
* @param {string[]} rotors - list of rotor spec strings (without step points!)
|
||||
* @param {Object} reflector - Reflector object
|
||||
* @param {string} ciphertext - The ciphertext to attack
|
||||
* @param {string} crib - Known plaintext for this ciphertext
|
||||
* @param {boolean} check - Whether to use the checking machine
|
||||
* @param {function} update - Function to call to send status updates (optional)
|
||||
*/
|
||||
constructor(rotors, reflector, ciphertext, crib, check, update=undefined) {
|
||||
if (ciphertext.length < crib.length) {
|
||||
throw new OperationError("Crib overruns supplied ciphertext");
|
||||
}
|
||||
if (crib.length < 2) {
|
||||
// This is the absolute bare minimum to be sane, and even then it's likely too short to
|
||||
// be useful
|
||||
throw new OperationError("Crib is too short");
|
||||
}
|
||||
if (crib.length > 25) {
|
||||
// A crib longer than this will definitely cause the middle rotor to step somewhere
|
||||
// A shorter crib is preferable to reduce this chance, of course
|
||||
throw new OperationError("Crib is too long");
|
||||
}
|
||||
for (let i=0; i<crib.length; i++) {
|
||||
if (ciphertext[i] === crib[i]) {
|
||||
throw new OperationError(`Invalid crib: character ${ciphertext[i]} at pos ${i} in both ciphertext and crib`);
|
||||
}
|
||||
}
|
||||
this.ciphertext = ciphertext;
|
||||
this.crib = crib;
|
||||
this.initRotors(rotors);
|
||||
this.check = check;
|
||||
this.updateFn = update;
|
||||
|
||||
const [mostConnected, edges] = this.makeMenu();
|
||||
|
||||
// This is the bundle of wires corresponding to the 26 letters within each of the 26
|
||||
// possible nodes in the menu
|
||||
this.wires = new Array(26*26);
|
||||
|
||||
// These are the pseudo-Engima devices corresponding to each edge in the menu, and the
|
||||
// nodes in the menu they each connect to
|
||||
this.scramblers = new Array();
|
||||
for (let i=0; i<26; i++) {
|
||||
this.scramblers.push(new Array());
|
||||
}
|
||||
this.sharedScrambler = new SharedScrambler(this.baseRotors.slice(1), reflector);
|
||||
this.allScramblers = new Array();
|
||||
this.indicator = undefined;
|
||||
for (const edge of edges) {
|
||||
const cRotor = this.baseRotors[0].copy();
|
||||
const end1 = a2i(edge.node1.letter);
|
||||
const end2 = a2i(edge.node2.letter);
|
||||
const scrambler = new Scrambler(this.sharedScrambler, cRotor, edge.pos, end1, end2);
|
||||
if (edge.pos === 0) {
|
||||
this.indicator = scrambler;
|
||||
}
|
||||
this.scramblers[end1].push(scrambler);
|
||||
this.scramblers[end2].push(scrambler);
|
||||
this.allScramblers.push(scrambler);
|
||||
}
|
||||
// The Bombe uses a set of rotors to keep track of what settings it's testing. We cheat and
|
||||
// use one of the actual scramblers if there's one in the right position, but if not we'll
|
||||
// just create one.
|
||||
if (this.indicator === undefined) {
|
||||
this.indicator = new Scrambler(this.sharedScrambler, this.baseRotors[0].copy(), 0, undefined, undefined);
|
||||
this.allScramblers.push(this.indicator);
|
||||
}
|
||||
|
||||
this.testRegister = a2i(mostConnected.letter);
|
||||
// This is an arbitrary letter other than the most connected letter
|
||||
for (const edge of mostConnected.edges) {
|
||||
this.testInput = [this.testRegister, a2i(edge.getOther(mostConnected).letter)];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build Rotor objects from list of rotor wiring strings.
|
||||
* @param {string[]} rotors - List of rotor wiring strings
|
||||
*/
|
||||
initRotors(rotors) {
|
||||
// This is ordered from the Enigma fast rotor to the slow, so bottom to top for the Bombe
|
||||
this.baseRotors = [];
|
||||
for (const rstr of rotors) {
|
||||
const rotor = new CopyRotor(rstr, "", "A", "A");
|
||||
this.baseRotors.push(rotor);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the rotors and reflector in all components of this Bombe.
|
||||
* @param {string[]} rotors - List of rotor wiring strings
|
||||
* @param {Object} reflector - Reflector object
|
||||
*/
|
||||
changeRotors(rotors, reflector) {
|
||||
// At the end of the run, the rotors are all back in the same position they started
|
||||
this.initRotors(rotors);
|
||||
this.sharedScrambler.changeRotors(this.baseRotors.slice(1), reflector);
|
||||
for (const scrambler of this.allScramblers) {
|
||||
scrambler.changeRotor(this.baseRotors[0].copy());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If we have a way of sending status messages, do so.
|
||||
* @param {...*} msg - Message to send.
|
||||
*/
|
||||
update(...msg) {
|
||||
if (this.updateFn !== undefined) {
|
||||
this.updateFn(...msg);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursive depth-first search on the menu graph.
|
||||
* This is used to a) isolate unconnected sub-graphs, and b) count the number of loops in each
|
||||
* of those graphs.
|
||||
* @param {Object} node - Node object to start the search from
|
||||
* @returns {[number, number, Object, number, Object[]} - loop count, node count, most connected
|
||||
* node, order of most connected node, list of edges in this sub-graph
|
||||
*/
|
||||
dfs(node) {
|
||||
let loops = 0;
|
||||
let nNodes = 1;
|
||||
let mostConnected = node;
|
||||
let nConnections = mostConnected.edges.size;
|
||||
let edges = new Set();
|
||||
node.visited = true;
|
||||
for (const edge of node.edges) {
|
||||
if (edge.visited) {
|
||||
// Already been here from the other end.
|
||||
continue;
|
||||
}
|
||||
edge.visited = true;
|
||||
edges.add(edge);
|
||||
const other = edge.getOther(node);
|
||||
if (other.visited) {
|
||||
// We have a loop, record that and continue
|
||||
loops += 1;
|
||||
continue;
|
||||
}
|
||||
// This is a newly visited node
|
||||
const [oLoops, oNNodes, oMostConnected, oNConnections, oEdges] = this.dfs(other);
|
||||
loops += oLoops;
|
||||
nNodes += oNNodes;
|
||||
edges = new Set([...edges, ...oEdges]);
|
||||
if (oNConnections > nConnections) {
|
||||
mostConnected = oMostConnected;
|
||||
nConnections = oNConnections;
|
||||
}
|
||||
}
|
||||
return [loops, nNodes, mostConnected, nConnections, edges];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a menu from the ciphertext and crib.
|
||||
* A menu is just a graph where letters in either the ciphertext or crib (Enigma is symmetric,
|
||||
* so there's no difference mathematically) are nodes and states of the Enigma machine itself
|
||||
* are the edges.
|
||||
* Additionally, we want a single connected graph, and of the subgraphs available, we want the
|
||||
* one with the most loops (since these generate feedback cycles which efficiently close off
|
||||
* disallowed states).
|
||||
* Finally, we want to identify the most connected node in that graph (as it's the best choice
|
||||
* of measurement point).
|
||||
* @returns [Object, Object[]] - the most connected node, and the list of edges in the subgraph
|
||||
*/
|
||||
makeMenu() {
|
||||
// First, we make a graph of all of the mappings given by the crib
|
||||
// Make all nodes first
|
||||
const nodes = new Map();
|
||||
for (const c of this.ciphertext + this.crib) {
|
||||
if (!nodes.has(c)) {
|
||||
const node = new Node(c);
|
||||
nodes.set(c, node);
|
||||
}
|
||||
}
|
||||
// Then all edges
|
||||
for (let i=0; i<this.crib.length; i++) {
|
||||
const a = this.crib[i];
|
||||
const b = this.ciphertext[i];
|
||||
new Edge(i, nodes.get(a), nodes.get(b));
|
||||
}
|
||||
// list of [loop_count, node_count, most_connected_node, connections_on_most_connected, edges]
|
||||
const graphs = [];
|
||||
// Then, for each unconnected subgraph, we count the number of loops and nodes
|
||||
for (const start of nodes.keys()) {
|
||||
if (nodes.get(start).visited) {
|
||||
continue;
|
||||
}
|
||||
const subgraph = this.dfs(nodes.get(start));
|
||||
graphs.push(subgraph);
|
||||
}
|
||||
// Return the subgraph with the most loops (ties broken by node count)
|
||||
graphs.sort((a, b) => {
|
||||
let result = b[0] - a[0];
|
||||
if (result === 0) {
|
||||
result = b[1] - a[1];
|
||||
}
|
||||
return result;
|
||||
});
|
||||
this.nLoops = graphs[0][0];
|
||||
return [graphs[0][2], graphs[0][4]];
|
||||
}
|
||||
|
||||
/**
|
||||
* Bombe electrical simulation. Energise a wire. For all connected wires (both via the diagonal
|
||||
* board and via the scramblers), energise them too, recursively.
|
||||
* @param {number} i - Bombe wire bundle
|
||||
* @param {number} j - Bombe stecker hypothesis wire within bundle
|
||||
*/
|
||||
energise(i, j) {
|
||||
const idx = 26*i + j;
|
||||
if (this.wires[idx]) {
|
||||
return;
|
||||
}
|
||||
this.wires[idx] = true;
|
||||
// Welchman's diagonal board: if A steckers to B, that implies B steckers to A. Handle
|
||||
// both.
|
||||
const idxPair = 26*j + i;
|
||||
this.wires[idxPair] = true;
|
||||
if (i === this.testRegister || j === this.testRegister) {
|
||||
this.energiseCount++;
|
||||
if (this.energiseCount === 26) {
|
||||
// no point continuing, bail out
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (let k=0; k<this.scramblers[i].length; k++) {
|
||||
const scrambler = this.scramblers[i][k];
|
||||
const out = scrambler.transform(j);
|
||||
const other = scrambler.getOtherEnd(i);
|
||||
// Lift the pre-check before the call, to save some function call overhead
|
||||
const otherIdx = 26*other + out;
|
||||
if (!this.wires[otherIdx]) {
|
||||
this.energise(other, out);
|
||||
if (this.energiseCount === 26) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (i === j) {
|
||||
return;
|
||||
}
|
||||
for (let k=0; k<this.scramblers[j].length; k++) {
|
||||
const scrambler = this.scramblers[j][k];
|
||||
const out = scrambler.transform(i);
|
||||
const other = scrambler.getOtherEnd(j);
|
||||
const otherIdx = 26*other + out;
|
||||
if (!this.wires[otherIdx]) {
|
||||
this.energise(other, out);
|
||||
if (this.energiseCount === 26) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trial decryption at the current setting.
|
||||
* Used after we get a stop.
|
||||
* This applies the detected stecker pair if we have one. It does not handle the other
|
||||
* steckering or stepping (which is why we limit it to 26 characters, since it's guaranteed to
|
||||
* be wrong after that anyway).
|
||||
* @param {string} stecker - Known stecker spec string.
|
||||
* @returns {string}
|
||||
*/
|
||||
tryDecrypt(stecker) {
|
||||
const fastRotor = this.indicator.rotor;
|
||||
const initialPos = fastRotor.pos;
|
||||
const res = [];
|
||||
const plugboard = new Plugboard(stecker);
|
||||
// The indicator scrambler starts in the right place for the beginning of the ciphertext.
|
||||
for (let i=0; i<Math.min(26, this.ciphertext.length); i++) {
|
||||
const t = this.indicator.transform(plugboard.transform(a2i(this.ciphertext[i])));
|
||||
res.push(i2a(plugboard.transform(t)));
|
||||
this.indicator.step(1);
|
||||
}
|
||||
fastRotor.pos = initialPos;
|
||||
return res.join("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a steckered pair, in sorted order to allow uniquing.
|
||||
* @param {number} a - A letter
|
||||
* @param {number} b - Its stecker pair
|
||||
* @returns {string}
|
||||
*/
|
||||
formatPair(a, b) {
|
||||
if (a < b) {
|
||||
return `${i2a(a)}${i2a(b)}`;
|
||||
}
|
||||
return `${i2a(b)}${i2a(a)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* The checking machine was used to manually verify Bombe stops. Using a device which was
|
||||
* effectively a non-stepping Enigma, the user would walk through each of the links in the
|
||||
* menu at the rotor positions determined by the Bombe. By starting with the stecker pair the
|
||||
* Bombe gives us, we find the stecker pair of each connected letter in the graph, and so on.
|
||||
* If a contradiction is reached, the stop is invalid. If not, we have most (but not
|
||||
* necessarily all) of the plugboard connections.
|
||||
* You will notice that this procedure is exactly the same as what the Bombe itself does, only
|
||||
* we start with an assumed good hypothesis and read out the stecker pair for every letter.
|
||||
* On the real hardware that wasn't practical, but fortunately we're not the real hardware, so
|
||||
* we don't need to implement the manual checking machine procedure.
|
||||
* @param {number} pair - The stecker pair of the test register.
|
||||
* @returns {string} - The empty string for invalid stops, or a plugboard configuration string
|
||||
* containing all known pairs.
|
||||
*/
|
||||
checkingMachine(pair) {
|
||||
if (pair !== this.testInput[1]) {
|
||||
// We have a new hypothesis for this stop - apply the new one.
|
||||
// De-energise the board
|
||||
for (let i=0; i<this.wires.length; i++) {
|
||||
this.wires[i] = false;
|
||||
}
|
||||
this.energiseCount = 0;
|
||||
// Re-energise with the corrected hypothesis
|
||||
this.energise(this.testRegister, pair);
|
||||
}
|
||||
|
||||
const results = new Set();
|
||||
results.add(this.formatPair(this.testRegister, pair));
|
||||
for (let i=0; i<26; i++) {
|
||||
let count = 0;
|
||||
let other;
|
||||
for (let j=0; j<26; j++) {
|
||||
if (this.wires[i*26 + j]) {
|
||||
count++;
|
||||
other = j;
|
||||
}
|
||||
}
|
||||
if (count > 1) {
|
||||
// This is an invalid stop.
|
||||
return "";
|
||||
} else if (count === 0) {
|
||||
// No information about steckering from this wire
|
||||
continue;
|
||||
}
|
||||
results.add(this.formatPair(i, other));
|
||||
}
|
||||
return [...results].join(" ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if the Bombe has stopped. If so, process the stop.
|
||||
* @returns {(undefined|string[3])} - Undefined for no stop, or [rotor settings, plugboard settings, decryption preview]
|
||||
*/
|
||||
checkStop() {
|
||||
// Count the energised outputs
|
||||
const count = this.energiseCount;
|
||||
if (count === 26) {
|
||||
return undefined;
|
||||
}
|
||||
// If it's not all of them, we have a stop
|
||||
let steckerPair;
|
||||
// The Bombe tells us one stecker pair as well. The input wire and test register we
|
||||
// started with are hypothesised to be a stecker pair.
|
||||
if (count === 25) {
|
||||
// Our steckering hypothesis is wrong. Correct value is the un-energised wire.
|
||||
for (let j=0; j<26; j++) {
|
||||
if (!this.wires[26*this.testRegister + j]) {
|
||||
steckerPair = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (count === 1) {
|
||||
// This means our hypothesis for the steckering is correct.
|
||||
steckerPair = this.testInput[1];
|
||||
} else {
|
||||
// This was known as a "boxing stop" - we have a stop but not a single hypothesis.
|
||||
// If this happens a lot it implies the menu isn't good enough.
|
||||
// If we have the checking machine enabled, we're going to just check each wire in
|
||||
// turn. If we get 0 or 1 hit, great.
|
||||
// If we get multiple hits, or the checking machine is off, the user will just have to
|
||||
// deal with it.
|
||||
if (!this.check) {
|
||||
// We can't draw any conclusions about the steckering (one could maybe suggest
|
||||
// options in some cases, but too hard to present clearly).
|
||||
return [this.indicator.getPos(), "??", this.tryDecrypt("")];
|
||||
}
|
||||
let stecker = undefined;
|
||||
for (let i = 0; i < 26; i++) {
|
||||
const newStecker = this.checkingMachine(i);
|
||||
if (newStecker !== "") {
|
||||
if (stecker !== undefined) {
|
||||
// Multiple hypotheses can't be ruled out.
|
||||
return [this.indicator.getPos(), "??", this.tryDecrypt("")];
|
||||
}
|
||||
stecker = newStecker;
|
||||
}
|
||||
}
|
||||
if (stecker === undefined) {
|
||||
// Checking machine ruled all possibilities out.
|
||||
return undefined;
|
||||
}
|
||||
// If we got here, there was just one possibility allowed by the checking machine. Success.
|
||||
return [this.indicator.getPos(), stecker, this.tryDecrypt(stecker)];
|
||||
}
|
||||
let stecker;
|
||||
if (this.check) {
|
||||
stecker = this.checkingMachine(steckerPair);
|
||||
if (stecker === "") {
|
||||
// Invalid stop - don't count it, don't return it
|
||||
return undefined;
|
||||
}
|
||||
} else {
|
||||
stecker = `${i2a(this.testRegister)}${i2a(steckerPair)}`;
|
||||
}
|
||||
const testDecrypt = this.tryDecrypt(stecker);
|
||||
return [this.indicator.getPos(), stecker, testDecrypt];
|
||||
}
|
||||
|
||||
/**
|
||||
* Having set up the Bombe, do the actual attack run. This tries every possible rotor setting
|
||||
* and attempts to logically invalidate them. If it can't, it's added to the list of candidate
|
||||
* solutions.
|
||||
* @returns {string[][3]} - list of 3-tuples of candidate rotor setting, plugboard settings, and decryption preview
|
||||
*/
|
||||
run() {
|
||||
let stops = 0;
|
||||
const result = [];
|
||||
// For each possible rotor setting
|
||||
const nChecks = Math.pow(26, this.baseRotors.length);
|
||||
for (let i=1; i<=nChecks; i++) {
|
||||
// Benchmarking suggests this is faster than using .fill()
|
||||
for (let i=0; i<this.wires.length; i++) {
|
||||
this.wires[i] = false;
|
||||
}
|
||||
this.energiseCount = 0;
|
||||
// Energise the test input, follow the current through each scrambler
|
||||
// (and the diagonal board)
|
||||
this.energise(...this.testInput);
|
||||
|
||||
const stop = this.checkStop();
|
||||
if (stop !== undefined) {
|
||||
stops++;
|
||||
result.push(stop);
|
||||
}
|
||||
// Step all the scramblers
|
||||
// This loop counts how many rotors have reached their starting position (meaning the
|
||||
// next one needs to step as well)
|
||||
let n = 1;
|
||||
for (let j=1; j<this.baseRotors.length; j++) {
|
||||
if ((i % Math.pow(26, j)) === 0) {
|
||||
n++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (n > 1) {
|
||||
this.sharedScrambler.step(n);
|
||||
}
|
||||
for (const scrambler of this.allScramblers) {
|
||||
scrambler.step();
|
||||
}
|
||||
// Send status messages at what seems to be a reasonably sensible frequency
|
||||
// (note this won't be triggered on 3-rotor runs - they run fast enough it doesn't seem necessary)
|
||||
if (n > 3) {
|
||||
this.update(this.nLoops, stops, i/nChecks);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
0
src/core/lib/CanvasComponents.mjs
Executable file → Normal file
0
src/core/lib/CanvasComponents.mjs
Executable file → Normal file
178
src/core/lib/Charts.mjs
Normal file
178
src/core/lib/Charts.mjs
Normal file
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* @author tlwr [toby@toby.codes]
|
||||
* @author Matt C [me@mitt.dev]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import OperationError from "../errors/OperationError";
|
||||
|
||||
/**
|
||||
* @constant
|
||||
* @default
|
||||
*/
|
||||
export const RECORD_DELIMITER_OPTIONS = ["Line feed", "CRLF"];
|
||||
|
||||
|
||||
/**
|
||||
* @constant
|
||||
* @default
|
||||
*/
|
||||
export const FIELD_DELIMITER_OPTIONS = ["Space", "Comma", "Semi-colon", "Colon", "Tab"];
|
||||
|
||||
|
||||
/**
|
||||
* Default from colour
|
||||
*
|
||||
* @constant
|
||||
* @default
|
||||
*/
|
||||
export const COLOURS = {
|
||||
min: "white",
|
||||
max: "black"
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Gets values from input for a plot.
|
||||
*
|
||||
* @param {string} input
|
||||
* @param {string} recordDelimiter
|
||||
* @param {string} fieldDelimiter
|
||||
* @param {boolean} columnHeadingsAreIncluded - whether we should skip the first record
|
||||
* @param {number} length
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
export function getValues(input, recordDelimiter, fieldDelimiter, columnHeadingsAreIncluded, length) {
|
||||
let headings;
|
||||
const values = [];
|
||||
|
||||
input
|
||||
.split(recordDelimiter)
|
||||
.forEach((row, rowIndex) => {
|
||||
const split = row.split(fieldDelimiter);
|
||||
if (split.length !== length) throw new OperationError(`Each row must have length ${length}.`);
|
||||
|
||||
if (columnHeadingsAreIncluded && rowIndex === 0) {
|
||||
headings = split;
|
||||
} else {
|
||||
values.push(split);
|
||||
}
|
||||
});
|
||||
return { headings, values };
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets values from input for a scatter plot.
|
||||
*
|
||||
* @param {string} input
|
||||
* @param {string} recordDelimiter
|
||||
* @param {string} fieldDelimiter
|
||||
* @param {boolean} columnHeadingsAreIncluded - whether we should skip the first record
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
export function getScatterValues(input, recordDelimiter, fieldDelimiter, columnHeadingsAreIncluded) {
|
||||
let { headings, values } = getValues(
|
||||
input,
|
||||
recordDelimiter,
|
||||
fieldDelimiter,
|
||||
columnHeadingsAreIncluded,
|
||||
2
|
||||
);
|
||||
|
||||
if (headings) {
|
||||
headings = {x: headings[0], y: headings[1]};
|
||||
}
|
||||
|
||||
values = values.map(row => {
|
||||
const x = parseFloat(row[0], 10),
|
||||
y = parseFloat(row[1], 10);
|
||||
|
||||
if (Number.isNaN(x)) throw new OperationError("Values must be numbers in base 10.");
|
||||
if (Number.isNaN(y)) throw new OperationError("Values must be numbers in base 10.");
|
||||
|
||||
return [x, y];
|
||||
});
|
||||
|
||||
return { headings, values };
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets values from input for a scatter plot with colour from the third column.
|
||||
*
|
||||
* @param {string} input
|
||||
* @param {string} recordDelimiter
|
||||
* @param {string} fieldDelimiter
|
||||
* @param {boolean} columnHeadingsAreIncluded - whether we should skip the first record
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
export function getScatterValuesWithColour(input, recordDelimiter, fieldDelimiter, columnHeadingsAreIncluded) {
|
||||
let { headings, values } = getValues(
|
||||
input,
|
||||
recordDelimiter, fieldDelimiter,
|
||||
columnHeadingsAreIncluded,
|
||||
3
|
||||
);
|
||||
|
||||
if (headings) {
|
||||
headings = {x: headings[0], y: headings[1]};
|
||||
}
|
||||
|
||||
values = values.map(row => {
|
||||
const x = parseFloat(row[0], 10),
|
||||
y = parseFloat(row[1], 10),
|
||||
colour = row[2];
|
||||
|
||||
if (Number.isNaN(x)) throw new OperationError("Values must be numbers in base 10.");
|
||||
if (Number.isNaN(y)) throw new OperationError("Values must be numbers in base 10.");
|
||||
|
||||
return [x, y, colour];
|
||||
});
|
||||
|
||||
return { headings, values };
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets values from input for a time series plot.
|
||||
*
|
||||
* @param {string} input
|
||||
* @param {string} recordDelimiter
|
||||
* @param {string} fieldDelimiter
|
||||
* @param {boolean} columnHeadingsAreIncluded - whether we should skip the first record
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
export function getSeriesValues(input, recordDelimiter, fieldDelimiter, columnHeadingsAreIncluded) {
|
||||
const { values } = getValues(
|
||||
input,
|
||||
recordDelimiter, fieldDelimiter,
|
||||
false,
|
||||
3
|
||||
);
|
||||
|
||||
let xValues = new Set();
|
||||
const series = {};
|
||||
|
||||
values.forEach(row => {
|
||||
const serie = row[0],
|
||||
xVal = row[1],
|
||||
val = parseFloat(row[2], 10);
|
||||
|
||||
if (Number.isNaN(val)) throw new OperationError("Values must be numbers in base 10.");
|
||||
|
||||
xValues.add(xVal);
|
||||
if (typeof series[serie] === "undefined") series[serie] = {};
|
||||
series[serie][xVal] = val;
|
||||
});
|
||||
|
||||
xValues = new Array(...xValues);
|
||||
|
||||
const seriesList = [];
|
||||
for (const seriesName in series) {
|
||||
const serie = series[seriesName];
|
||||
seriesList.push({name: seriesName, data: serie});
|
||||
}
|
||||
|
||||
return { xValues, series: seriesList };
|
||||
}
|
||||
369
src/core/lib/Enigma.mjs
Normal file
369
src/core/lib/Enigma.mjs
Normal file
@@ -0,0 +1,369 @@
|
||||
/**
|
||||
* Emulation of the Enigma machine.
|
||||
*
|
||||
* @author s2224834
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
import OperationError from "../errors/OperationError";
|
||||
import Utils from "../Utils";
|
||||
|
||||
/**
|
||||
* Provided default Enigma rotor set.
|
||||
* These are specified as a list of mappings from the letters A through Z in order, optionally
|
||||
* followed by < and a list of letters at which the rotor steps.
|
||||
*/
|
||||
export const ROTORS = [
|
||||
{name: "I", value: "EKMFLGDQVZNTOWYHXUSPAIBRCJ<R"},
|
||||
{name: "II", value: "AJDKSIRUXBLHWTMCQGZNPYFVOE<F"},
|
||||
{name: "III", value: "BDFHJLCPRTXVZNYEIWGAKMUSQO<W"},
|
||||
{name: "IV", value: "ESOVPZJAYQUIRHXLNFTGKDCMWB<K"},
|
||||
{name: "V", value: "VZBRGITYUPSDNHLXAWMJQOFECK<A"},
|
||||
{name: "VI", value: "JPGVOUMFYQBENHZRDKASXLICTW<AN"},
|
||||
{name: "VII", value: "NZJHGRCXMYSWBOUFAIVLPEKQDT<AN"},
|
||||
{name: "VIII", value: "FKQHTLXOCBJSPDZRAMEWNIUYGV<AN"},
|
||||
];
|
||||
|
||||
export const ROTORS_FOURTH = [
|
||||
{name: "Beta", value: "LEYJVCNIXWPBQMDRTAKZGFUHOS"},
|
||||
{name: "Gamma", value: "FSOKANUERHMBTIYCWLQPZXVGJD"},
|
||||
];
|
||||
|
||||
/**
|
||||
* Provided default Enigma reflector set.
|
||||
* These are specified as 13 space-separated transposed pairs covering every letter.
|
||||
*/
|
||||
export const REFLECTORS = [
|
||||
{name: "B", value: "AY BR CU DH EQ FS GL IP JX KN MO TZ VW"},
|
||||
{name: "C", value: "AF BV CP DJ EI GO HY KR LZ MX NW TQ SU"},
|
||||
{name: "B Thin", value: "AE BN CK DQ FU GY HW IJ LO MP RX SZ TV"},
|
||||
{name: "C Thin", value: "AR BD CO EJ FN GT HK IV LM PW QZ SX UY"},
|
||||
];
|
||||
|
||||
export const LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
|
||||
|
||||
/**
|
||||
* Map a letter to a number in 0..25.
|
||||
*
|
||||
* @param {char} c
|
||||
* @param {boolean} permissive - Case insensitive; don't throw errors on other chars.
|
||||
* @returns {number}
|
||||
*/
|
||||
export function a2i(c, permissive=false) {
|
||||
const i = Utils.ord(c);
|
||||
if (i >= 65 && i <= 90) {
|
||||
return i - 65;
|
||||
}
|
||||
if (permissive) {
|
||||
// Allow case insensitivity
|
||||
if (i >= 97 && i <= 122) {
|
||||
return i - 97;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
throw new OperationError("a2i called on non-uppercase ASCII character");
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a number in 0..25 to a letter.
|
||||
*
|
||||
* @param {number} i
|
||||
* @returns {char}
|
||||
*/
|
||||
export function i2a(i) {
|
||||
if (i >= 0 && i < 26) {
|
||||
return Utils.chr(i+65);
|
||||
}
|
||||
throw new OperationError("i2a called on value outside 0..25");
|
||||
}
|
||||
|
||||
/**
|
||||
* A rotor in the Enigma machine.
|
||||
*/
|
||||
export class Rotor {
|
||||
/**
|
||||
* Rotor constructor.
|
||||
*
|
||||
* @param {string} wiring - A 26 character string of the wiring order.
|
||||
* @param {string} steps - A 0..26 character string of stepping points.
|
||||
* @param {char} ringSetting - The ring setting.
|
||||
* @param {char} initialPosition - The initial position of the rotor.
|
||||
*/
|
||||
constructor(wiring, steps, ringSetting, initialPosition) {
|
||||
if (!/^[A-Z]{26}$/.test(wiring)) {
|
||||
throw new OperationError("Rotor wiring must be 26 unique uppercase letters");
|
||||
}
|
||||
if (!/^[A-Z]{0,26}$/.test(steps)) {
|
||||
throw new OperationError("Rotor steps must be 0-26 unique uppercase letters");
|
||||
}
|
||||
if (!/^[A-Z]$/.test(ringSetting)) {
|
||||
throw new OperationError("Rotor ring setting must be exactly one uppercase letter");
|
||||
}
|
||||
if (!/^[A-Z]$/.test(initialPosition)) {
|
||||
throw new OperationError("Rotor initial position must be exactly one uppercase letter");
|
||||
}
|
||||
this.map = new Array(26);
|
||||
this.revMap = new Array(26);
|
||||
const uniq = {};
|
||||
for (let i=0; i<LETTERS.length; i++) {
|
||||
const a = a2i(LETTERS[i]);
|
||||
const b = a2i(wiring[i]);
|
||||
this.map[a] = b;
|
||||
this.revMap[b] = a;
|
||||
uniq[b] = true;
|
||||
}
|
||||
if (Object.keys(uniq).length !== LETTERS.length) {
|
||||
throw new OperationError("Rotor wiring must have each letter exactly once");
|
||||
}
|
||||
const rs = a2i(ringSetting);
|
||||
this.steps = new Set();
|
||||
for (const x of steps) {
|
||||
this.steps.add(Utils.mod(a2i(x) - rs, 26));
|
||||
}
|
||||
if (this.steps.size !== steps.length) {
|
||||
// This isn't strictly fatal, but it's probably a mistake
|
||||
throw new OperationError("Rotor steps must be unique");
|
||||
}
|
||||
this.pos = Utils.mod(a2i(initialPosition) - rs, 26);
|
||||
}
|
||||
|
||||
/**
|
||||
* Step the rotor forward by one.
|
||||
*/
|
||||
step() {
|
||||
this.pos = Utils.mod(this.pos + 1, 26);
|
||||
return this.pos;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a character through this rotor forwards.
|
||||
*
|
||||
* @param {number} c - The character.
|
||||
* @returns {number}
|
||||
*/
|
||||
transform(c) {
|
||||
return Utils.mod(this.map[Utils.mod(c + this.pos, 26)] - this.pos, 26);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a character through this rotor backwards.
|
||||
*
|
||||
* @param {number} c - The character.
|
||||
* @returns {number}
|
||||
*/
|
||||
revTransform(c) {
|
||||
return Utils.mod(this.revMap[Utils.mod(c + this.pos, 26)] - this.pos, 26);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for plugboard and reflector (since these do effectively the same
|
||||
* thing).
|
||||
*/
|
||||
class PairMapBase {
|
||||
/**
|
||||
* PairMapBase constructor.
|
||||
*
|
||||
* @param {string} pairs - A whitespace separated string of letter pairs to swap.
|
||||
* @param {string} [name='PairMapBase'] - For errors, the name of this object.
|
||||
*/
|
||||
constructor(pairs, name="PairMapBase") {
|
||||
// I've chosen to make whitespace significant here to make a) code and
|
||||
// b) inputs easier to read
|
||||
this.pairs = pairs;
|
||||
this.map = {};
|
||||
if (pairs === "") {
|
||||
return;
|
||||
}
|
||||
pairs.split(/\s+/).forEach(pair => {
|
||||
if (!/^[A-Z]{2}$/.test(pair)) {
|
||||
throw new OperationError(name + " must be a whitespace-separated list of uppercase letter pairs");
|
||||
}
|
||||
const a = a2i(pair[0]), b = a2i(pair[1]);
|
||||
if (a === b) {
|
||||
// self-stecker
|
||||
return;
|
||||
}
|
||||
if (this.map.hasOwnProperty(a)) {
|
||||
throw new OperationError(`${name} connects ${pair[0]} more than once`);
|
||||
}
|
||||
if (this.map.hasOwnProperty(b)) {
|
||||
throw new OperationError(`${name} connects ${pair[1]} more than once`);
|
||||
}
|
||||
this.map[a] = b;
|
||||
this.map[b] = a;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a character through this object.
|
||||
* Returns other characters unchanged.
|
||||
*
|
||||
* @param {number} c - The character.
|
||||
* @returns {number}
|
||||
*/
|
||||
transform(c) {
|
||||
if (!this.map.hasOwnProperty(c)) {
|
||||
return c;
|
||||
}
|
||||
return this.map[c];
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for transform, to allow interchangeable use with rotors.
|
||||
*
|
||||
* @param {number} c - The character.
|
||||
* @returns {number}
|
||||
*/
|
||||
revTransform(c) {
|
||||
return this.transform(c);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reflector. PairMapBase but requires that all characters are accounted for.
|
||||
*
|
||||
* Includes a couple of optimisations on that basis.
|
||||
*/
|
||||
export class Reflector extends PairMapBase {
|
||||
/**
|
||||
* Reflector constructor. See PairMapBase.
|
||||
* Additional restriction: every character must be accounted for.
|
||||
*/
|
||||
constructor(pairs) {
|
||||
super(pairs, "Reflector");
|
||||
const s = Object.keys(this.map).length;
|
||||
if (s !== 26) {
|
||||
throw new OperationError("Reflector must have exactly 13 pairs covering every letter");
|
||||
}
|
||||
const optMap = new Array(26);
|
||||
for (const x of Object.keys(this.map)) {
|
||||
optMap[x] = this.map[x];
|
||||
}
|
||||
this.map = optMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a character through this object.
|
||||
*
|
||||
* @param {number} c - The character.
|
||||
* @returns {number}
|
||||
*/
|
||||
transform(c) {
|
||||
return this.map[c];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugboard. Unmodified PairMapBase.
|
||||
*/
|
||||
export class Plugboard extends PairMapBase {
|
||||
/**
|
||||
* Plugboard constructor. See PairMapbase.
|
||||
*/
|
||||
constructor(pairs) {
|
||||
super(pairs, "Plugboard");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Base class for the Enigma machine itself. Holds rotors, a reflector, and a plugboard.
|
||||
*/
|
||||
export class EnigmaBase {
|
||||
/**
|
||||
* EnigmaBase constructor.
|
||||
*
|
||||
* @param {Object[]} rotors - List of Rotors.
|
||||
* @param {Object} reflector - A Reflector.
|
||||
* @param {Plugboard} plugboard - A Plugboard.
|
||||
*/
|
||||
constructor(rotors, reflector, plugboard) {
|
||||
this.rotors = rotors;
|
||||
this.rotorsRev = [].concat(rotors).reverse();
|
||||
this.reflector = reflector;
|
||||
this.plugboard = plugboard;
|
||||
}
|
||||
|
||||
/**
|
||||
* Step the rotors forward by one.
|
||||
*
|
||||
* This happens before the output character is generated.
|
||||
*
|
||||
* Note that rotor 4, if it's there, never steps.
|
||||
*
|
||||
* Why is all the logic in EnigmaBase and not a nice neat method on
|
||||
* Rotor that knows when it should advance the next item?
|
||||
* Because the double stepping anomaly is a thing. tl;dr if the left rotor
|
||||
* should step the next time the middle rotor steps, the middle rotor will
|
||||
* immediately step.
|
||||
*/
|
||||
step() {
|
||||
const r0 = this.rotors[0];
|
||||
const r1 = this.rotors[1];
|
||||
r0.step();
|
||||
// The second test here is the double-stepping anomaly
|
||||
if (r0.steps.has(r0.pos) || r1.steps.has(Utils.mod(r1.pos + 1, 26))) {
|
||||
r1.step();
|
||||
if (r1.steps.has(r1.pos)) {
|
||||
const r2 = this.rotors[2];
|
||||
r2.step();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt (or decrypt) some data.
|
||||
* Takes an arbitrary string and runs the Engima machine on that data from
|
||||
* *its current state*, and outputs the result. Non-alphabetic characters
|
||||
* are returned unchanged.
|
||||
*
|
||||
* @param {string} input - Data to encrypt.
|
||||
* @returns {string}
|
||||
*/
|
||||
crypt(input) {
|
||||
let result = "";
|
||||
for (const c of input) {
|
||||
let letter = a2i(c, true);
|
||||
if (letter === -1) {
|
||||
result += c;
|
||||
continue;
|
||||
}
|
||||
// First, step the rotors forward.
|
||||
this.step();
|
||||
// Now, run through the plugboard.
|
||||
letter = this.plugboard.transform(letter);
|
||||
// Then through each wheel in sequence, through the reflector, and
|
||||
// backwards through the wheels again.
|
||||
for (const rotor of this.rotors) {
|
||||
letter = rotor.transform(letter);
|
||||
}
|
||||
letter = this.reflector.transform(letter);
|
||||
for (const rotor of this.rotorsRev) {
|
||||
letter = rotor.revTransform(letter);
|
||||
}
|
||||
// Finally, back through the plugboard.
|
||||
letter = this.plugboard.revTransform(letter);
|
||||
result += i2a(letter);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The Enigma machine itself. Holds 3-4 rotors, a reflector, and a plugboard.
|
||||
*/
|
||||
export class EnigmaMachine extends EnigmaBase {
|
||||
/**
|
||||
* EnigmaMachine constructor.
|
||||
*
|
||||
* @param {Object[]} rotors - List of Rotors.
|
||||
* @param {Object} reflector - A Reflector.
|
||||
* @param {Plugboard} plugboard - A Plugboard.
|
||||
*/
|
||||
constructor(rotors, reflector, plugboard) {
|
||||
super(rotors, reflector, plugboard);
|
||||
if (rotors.length !== 3 && rotors.length !== 4) {
|
||||
throw new OperationError("Enigma must have 3 or 4 rotors");
|
||||
}
|
||||
}
|
||||
}
|
||||
1727
src/core/lib/FileSignatures.mjs
Normal file
1727
src/core/lib/FileSignatures.mjs
Normal file
File diff suppressed because it is too large
Load Diff
263
src/core/lib/FileType.mjs
Normal file
263
src/core/lib/FileType.mjs
Normal file
@@ -0,0 +1,263 @@
|
||||
/**
|
||||
* File type functions
|
||||
*
|
||||
* @author n1474335 [n1474335@gmail.com]
|
||||
* @copyright Crown Copyright 2018
|
||||
* @license Apache-2.0
|
||||
*
|
||||
*/
|
||||
import {FILE_SIGNATURES} from "./FileSignatures";
|
||||
import {sendStatusMessage} from "../Utils";
|
||||
|
||||
|
||||
/**
|
||||
* Checks whether a signature matches a buffer.
|
||||
*
|
||||
* @param {Object|Object[]} sig - A dictionary of offsets with values assigned to them.
|
||||
* These values can be numbers for static checks, arrays of potential valid matches,
|
||||
* or bespoke functions to check the validity of the buffer value at that offset.
|
||||
* @param {Uint8Array} buf
|
||||
* @param {number} [offset=0] Where in the buffer to start searching from
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function signatureMatches(sig, buf, offset=0) {
|
||||
// Using a length check seems to be more performant than `sig instanceof Array`
|
||||
if (sig.length) {
|
||||
// sig is an Array - return true if any of them match
|
||||
// The following `reduce` method is nice, but performance matters here, so we
|
||||
// opt for a faster, if less elegant, for loop.
|
||||
// return sig.reduce((acc, s) => acc || bytesMatch(s, buf, offset), false);
|
||||
for (let i = 0; i < sig.length; i++) {
|
||||
if (bytesMatch(sig[i], buf, offset)) return true;
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
return bytesMatch(sig, buf, offset);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks whether a set of bytes match the given buffer.
|
||||
*
|
||||
* @param {Object} sig - A dictionary of offsets with values assigned to them.
|
||||
* These values can be numbers for static checks, arrays of potential valid matches,
|
||||
* or bespoke functions to check the validity of the buffer value at that offset.
|
||||
* @param {Uint8Array} buf
|
||||
* @param {number} [offset=0] Where in the buffer to start searching from
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function bytesMatch(sig, buf, offset=0) {
|
||||
for (const sigoffset in sig) {
|
||||
const pos = parseInt(sigoffset, 10) + offset;
|
||||
switch (typeof sig[sigoffset]) {
|
||||
case "number": // Static check
|
||||
if (buf[pos] !== sig[sigoffset])
|
||||
return false;
|
||||
break;
|
||||
case "object": // Array of options
|
||||
if (sig[sigoffset].indexOf(buf[pos]) < 0)
|
||||
return false;
|
||||
break;
|
||||
case "function": // More complex calculation
|
||||
if (!sig[sigoffset](buf[pos]))
|
||||
return false;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unrecognised signature type at offset ${sigoffset}`);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Given a buffer, detects magic byte sequences at specific positions and returns the
|
||||
* extension and mime type.
|
||||
*
|
||||
* @param {Uint8Array} buf
|
||||
* @param {string[]} [categories=All] - Which categories of file to look for
|
||||
* @returns {Object[]} types
|
||||
* @returns {string} type.name - Name of file type
|
||||
* @returns {string} type.ext - File extension
|
||||
* @returns {string} type.mime - Mime type
|
||||
* @returns {string} [type.desc] - Description
|
||||
*/
|
||||
export function detectFileType(buf, categories=Object.keys(FILE_SIGNATURES)) {
|
||||
if (!(buf && buf.length > 1)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const matchingFiles = [];
|
||||
const signatures = {};
|
||||
|
||||
for (const cat in FILE_SIGNATURES) {
|
||||
if (categories.includes(cat)) {
|
||||
signatures[cat] = FILE_SIGNATURES[cat];
|
||||
}
|
||||
}
|
||||
|
||||
for (const cat in signatures) {
|
||||
const category = signatures[cat];
|
||||
|
||||
category.forEach(filetype => {
|
||||
if (signatureMatches(filetype.signature, buf)) {
|
||||
matchingFiles.push(filetype);
|
||||
}
|
||||
});
|
||||
}
|
||||
return matchingFiles;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Given a buffer, searches for magic byte sequences at all possible positions and returns
|
||||
* the extensions and mime types.
|
||||
*
|
||||
* @param {Uint8Array} buf
|
||||
* @param {string[]} [categories=All] - Which categories of file to look for
|
||||
* @returns {Object[]} foundFiles
|
||||
* @returns {number} foundFiles.offset - The position in the buffer at which this file was found
|
||||
* @returns {Object} foundFiles.fileDetails
|
||||
* @returns {string} foundFiles.fileDetails.name - Name of file type
|
||||
* @returns {string} foundFiles.fileDetails.ext - File extension
|
||||
* @returns {string} foundFiles.fileDetails.mime - Mime type
|
||||
* @returns {string} [foundFiles.fileDetails.desc] - Description
|
||||
*/
|
||||
export function scanForFileTypes(buf, categories=Object.keys(FILE_SIGNATURES)) {
|
||||
if (!(buf && buf.length > 1)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const foundFiles = [];
|
||||
const signatures = {};
|
||||
|
||||
for (const cat in FILE_SIGNATURES) {
|
||||
if (categories.includes(cat)) {
|
||||
signatures[cat] = FILE_SIGNATURES[cat];
|
||||
}
|
||||
}
|
||||
|
||||
for (const cat in signatures) {
|
||||
const category = signatures[cat];
|
||||
|
||||
for (let i = 0; i < category.length; i++) {
|
||||
const filetype = category[i];
|
||||
const sigs = filetype.signature.length ? filetype.signature : [filetype.signature];
|
||||
|
||||
sigs.forEach(sig => {
|
||||
let pos = 0;
|
||||
while ((pos = locatePotentialSig(buf, sig, pos)) >= 0) {
|
||||
if (bytesMatch(sig, buf, pos)) {
|
||||
sendStatusMessage(`Found potential signature for ${filetype.name} at pos ${pos}`);
|
||||
foundFiles.push({
|
||||
offset: pos,
|
||||
fileDetails: filetype
|
||||
});
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Return found files in order of increasing offset
|
||||
return foundFiles.sort((a, b) => {
|
||||
return a.offset - b.offset;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Fastcheck function to quickly scan the buffer for the first byte in a signature.
|
||||
*
|
||||
* @param {Uint8Array} buf - The buffer to search
|
||||
* @param {Object} sig - A single signature object (Not an array of signatures)
|
||||
* @param {number} offset - Where to start search from
|
||||
* @returs {number} The position of the match or -1 if one cannot be found.
|
||||
*/
|
||||
function locatePotentialSig(buf, sig, offset) {
|
||||
// Find values for first key and value in sig
|
||||
const k = parseInt(Object.keys(sig)[0], 10);
|
||||
const v = Object.values(sig)[0];
|
||||
switch (typeof v) {
|
||||
case "number":
|
||||
return buf.indexOf(v, offset + k) - k;
|
||||
case "object":
|
||||
for (let i = offset + k; i < buf.length; i++) {
|
||||
if (v.indexOf(buf[i]) >= 0) return i - k;
|
||||
}
|
||||
return -1;
|
||||
case "function":
|
||||
for (let i = offset + k; i < buf.length; i++) {
|
||||
if (v(buf[i])) return i - k;
|
||||
}
|
||||
return -1;
|
||||
default:
|
||||
throw new Error("Unrecognised signature type");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Detects whether the given buffer is a file of the type specified.
|
||||
*
|
||||
* @param {string|RegExp} type
|
||||
* @param {Uint8Array} buf
|
||||
* @returns {string|false} The mime type or false if the type does not match
|
||||
*/
|
||||
export function isType(type, buf) {
|
||||
const types = detectFileType(buf);
|
||||
|
||||
if (!(types && types.length)) return false;
|
||||
|
||||
if (typeof type === "string") {
|
||||
return types.reduce((acc, t) => {
|
||||
const mime = t.mime.startsWith(type) ? t.mime : false;
|
||||
return acc || mime;
|
||||
}, false);
|
||||
} else if (type instanceof RegExp) {
|
||||
return types.reduce((acc, t) => {
|
||||
const mime = type.test(t.mime) ? t.mime : false;
|
||||
return acc || mime;
|
||||
}, false);
|
||||
} else {
|
||||
throw new Error("Invalid type input.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Detects whether the given buffer contains an image file.
|
||||
*
|
||||
* @param {Uint8Array} buf
|
||||
* @returns {string|false} The mime type or false if the type does not match
|
||||
*/
|
||||
export function isImage(buf) {
|
||||
return isType("image", buf);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Attempts to extract a file from a data stream given its offset and extractor function.
|
||||
*
|
||||
* @param {Uint8Array} bytes
|
||||
* @param {Object} fileDetail
|
||||
* @param {string} fileDetail.mime
|
||||
* @param {string} fileDetail.extension
|
||||
* @param {Function} fileDetail.extractor
|
||||
* @param {number} offset
|
||||
* @returns {File}
|
||||
*/
|
||||
export function extractFile(bytes, fileDetail, offset) {
|
||||
if (fileDetail.extractor) {
|
||||
sendStatusMessage(`Attempting to extract ${fileDetail.name} at pos ${offset}...`);
|
||||
const fileData = fileDetail.extractor(bytes, offset);
|
||||
const ext = fileDetail.extension.split(",")[0];
|
||||
return new File([fileData], `extracted_at_0x${offset.toString(16)}.${ext}`, {
|
||||
type: fileDetail.mime
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error(`No extraction algorithm available for "${fileDetail.mime}" files`);
|
||||
}
|
||||
@@ -100,7 +100,7 @@ export function fromHex(data, delim="Auto", byteLen=2) {
|
||||
/**
|
||||
* To Hexadecimal delimiters.
|
||||
*/
|
||||
export const TO_HEX_DELIM_OPTIONS = ["Space", "Comma", "Semi-colon", "Colon", "Line feed", "CRLF", "0x", "\\x", "None"];
|
||||
export const TO_HEX_DELIM_OPTIONS = ["Space", "Percent", "Comma", "Semi-colon", "Colon", "Line feed", "CRLF", "0x", "\\x", "None"];
|
||||
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,6 +2,7 @@ import OperationConfig from "../config/OperationConfig.json";
|
||||
import Utils from "../Utils";
|
||||
import Recipe from "../Recipe";
|
||||
import Dish from "../Dish";
|
||||
import {detectFileType} from "./FileType";
|
||||
import chiSquared from "chi-squared";
|
||||
|
||||
/**
|
||||
@@ -92,7 +93,14 @@ class Magic {
|
||||
* @returns {string} [type.desc] - Description
|
||||
*/
|
||||
detectFileType() {
|
||||
return Magic.magicFileType(this.inputBuffer);
|
||||
const fileType = detectFileType(this.inputBuffer);
|
||||
|
||||
if (!fileType.length) return null;
|
||||
return {
|
||||
ext: fileType[0].extension,
|
||||
mime: fileType[0].mime,
|
||||
desc: fileType[0].description
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -785,452 +793,9 @@ class Magic {
|
||||
}[code];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Given a buffer, detects magic byte sequences at specific positions and returns the
|
||||
* extension and mime type.
|
||||
*
|
||||
* @param {Uint8Array} buf
|
||||
* @returns {Object} type
|
||||
* @returns {string} type.ext - File extension
|
||||
* @returns {string} type.mime - Mime type
|
||||
* @returns {string} [type.desc] - Description
|
||||
*/
|
||||
static magicFileType(buf) {
|
||||
if (!(buf && buf.length > 1)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (buf[0] === 0xFF && buf[1] === 0xD8 && buf[2] === 0xFF) {
|
||||
return {
|
||||
ext: "jpg",
|
||||
mime: "image/jpeg"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4E && buf[3] === 0x47) {
|
||||
return {
|
||||
ext: "png",
|
||||
mime: "image/png"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x47 && buf[1] === 0x49 && buf[2] === 0x46) {
|
||||
return {
|
||||
ext: "gif",
|
||||
mime: "image/gif"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[8] === 0x57 && buf[9] === 0x45 && buf[10] === 0x42 && buf[11] === 0x50) {
|
||||
return {
|
||||
ext: "webp",
|
||||
mime: "image/webp"
|
||||
};
|
||||
}
|
||||
|
||||
// needs to be before `tif` check
|
||||
if (((buf[0] === 0x49 && buf[1] === 0x49 && buf[2] === 0x2A && buf[3] === 0x0) || (buf[0] === 0x4D && buf[1] === 0x4D && buf[2] === 0x0 && buf[3] === 0x2A)) && buf[8] === 0x43 && buf[9] === 0x52) {
|
||||
return {
|
||||
ext: "cr2",
|
||||
mime: "image/x-canon-cr2"
|
||||
};
|
||||
}
|
||||
|
||||
if ((buf[0] === 0x49 && buf[1] === 0x49 && buf[2] === 0x2A && buf[3] === 0x0) || (buf[0] === 0x4D && buf[1] === 0x4D && buf[2] === 0x0 && buf[3] === 0x2A)) {
|
||||
return {
|
||||
ext: "tif",
|
||||
mime: "image/tiff"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x42 && buf[1] === 0x4D) {
|
||||
return {
|
||||
ext: "bmp",
|
||||
mime: "image/bmp"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x49 && buf[1] === 0x49 && buf[2] === 0xBC) {
|
||||
return {
|
||||
ext: "jxr",
|
||||
mime: "image/vnd.ms-photo"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x38 && buf[1] === 0x42 && buf[2] === 0x50 && buf[3] === 0x53) {
|
||||
return {
|
||||
ext: "psd",
|
||||
mime: "image/vnd.adobe.photoshop"
|
||||
};
|
||||
}
|
||||
|
||||
// needs to be before `zip` check
|
||||
if (buf[0] === 0x50 && buf[1] === 0x4B && buf[2] === 0x3 && buf[3] === 0x4 && buf[30] === 0x6D && buf[31] === 0x69 && buf[32] === 0x6D && buf[33] === 0x65 && buf[34] === 0x74 && buf[35] === 0x79 && buf[36] === 0x70 && buf[37] === 0x65 && buf[38] === 0x61 && buf[39] === 0x70 && buf[40] === 0x70 && buf[41] === 0x6C && buf[42] === 0x69 && buf[43] === 0x63 && buf[44] === 0x61 && buf[45] === 0x74 && buf[46] === 0x69 && buf[47] === 0x6F && buf[48] === 0x6E && buf[49] === 0x2F && buf[50] === 0x65 && buf[51] === 0x70 && buf[52] === 0x75 && buf[53] === 0x62 && buf[54] === 0x2B && buf[55] === 0x7A && buf[56] === 0x69 && buf[57] === 0x70) {
|
||||
return {
|
||||
ext: "epub",
|
||||
mime: "application/epub+zip"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x50 && buf[1] === 0x4B && (buf[2] === 0x3 || buf[2] === 0x5 || buf[2] === 0x7) && (buf[3] === 0x4 || buf[3] === 0x6 || buf[3] === 0x8)) {
|
||||
return {
|
||||
ext: "zip",
|
||||
mime: "application/zip"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[257] === 0x75 && buf[258] === 0x73 && buf[259] === 0x74 && buf[260] === 0x61 && buf[261] === 0x72) {
|
||||
return {
|
||||
ext: "tar",
|
||||
mime: "application/x-tar"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x52 && buf[1] === 0x61 && buf[2] === 0x72 && buf[3] === 0x21 && buf[4] === 0x1A && buf[5] === 0x7 && (buf[6] === 0x0 || buf[6] === 0x1)) {
|
||||
return {
|
||||
ext: "rar",
|
||||
mime: "application/x-rar-compressed"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x1F && buf[1] === 0x8B && buf[2] === 0x8) {
|
||||
return {
|
||||
ext: "gz",
|
||||
mime: "application/gzip"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x42 && buf[1] === 0x5A && buf[2] === 0x68) {
|
||||
return {
|
||||
ext: "bz2",
|
||||
mime: "application/x-bzip2"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x37 && buf[1] === 0x7A && buf[2] === 0xBC && buf[3] === 0xAF && buf[4] === 0x27 && buf[5] === 0x1C) {
|
||||
return {
|
||||
ext: "7z",
|
||||
mime: "application/x-7z-compressed"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x78 && buf[1] === 0x01) {
|
||||
return {
|
||||
ext: "dmg, zlib",
|
||||
mime: "application/x-apple-diskimage, application/x-deflate"
|
||||
};
|
||||
}
|
||||
|
||||
if ((buf[0] === 0x0 && buf[1] === 0x0 && buf[2] === 0x0 && (buf[3] === 0x18 || buf[3] === 0x20) && buf[4] === 0x66 && buf[5] === 0x74 && buf[6] === 0x79 && buf[7] === 0x70) || (buf[0] === 0x33 && buf[1] === 0x67 && buf[2] === 0x70 && buf[3] === 0x35) || (buf[0] === 0x0 && buf[1] === 0x0 && buf[2] === 0x0 && buf[3] === 0x1C && buf[4] === 0x66 && buf[5] === 0x74 && buf[6] === 0x79 && buf[7] === 0x70 && buf[8] === 0x6D && buf[9] === 0x70 && buf[10] === 0x34 && buf[11] === 0x32 && buf[16] === 0x6D && buf[17] === 0x70 && buf[18] === 0x34 && buf[19] === 0x31 && buf[20] === 0x6D && buf[21] === 0x70 && buf[22] === 0x34 && buf[23] === 0x32 && buf[24] === 0x69 && buf[25] === 0x73 && buf[26] === 0x6F && buf[27] === 0x6D)) {
|
||||
return {
|
||||
ext: "mp4",
|
||||
mime: "video/mp4"
|
||||
};
|
||||
}
|
||||
|
||||
if ((buf[0] === 0x0 && buf[1] === 0x0 && buf[2] === 0x0 && buf[3] === 0x1C && buf[4] === 0x66 && buf[5] === 0x74 && buf[6] === 0x79 && buf[7] === 0x70 && buf[8] === 0x4D && buf[9] === 0x34 && buf[10] === 0x56)) {
|
||||
return {
|
||||
ext: "m4v",
|
||||
mime: "video/x-m4v"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x4D && buf[1] === 0x54 && buf[2] === 0x68 && buf[3] === 0x64) {
|
||||
return {
|
||||
ext: "mid",
|
||||
mime: "audio/midi"
|
||||
};
|
||||
}
|
||||
|
||||
// needs to be before the `webm` check
|
||||
if (buf[31] === 0x6D && buf[32] === 0x61 && buf[33] === 0x74 && buf[34] === 0x72 && buf[35] === 0x6f && buf[36] === 0x73 && buf[37] === 0x6B && buf[38] === 0x61) {
|
||||
return {
|
||||
ext: "mkv",
|
||||
mime: "video/x-matroska"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x1A && buf[1] === 0x45 && buf[2] === 0xDF && buf[3] === 0xA3) {
|
||||
return {
|
||||
ext: "webm",
|
||||
mime: "video/webm"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x0 && buf[1] === 0x0 && buf[2] === 0x0 && buf[3] === 0x14 && buf[4] === 0x66 && buf[5] === 0x74 && buf[6] === 0x79 && buf[7] === 0x70) {
|
||||
return {
|
||||
ext: "mov",
|
||||
mime: "video/quicktime"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x52 && buf[1] === 0x49 && buf[2] === 0x46 && buf[3] === 0x46 && buf[8] === 0x41 && buf[9] === 0x56 && buf[10] === 0x49) {
|
||||
return {
|
||||
ext: "avi",
|
||||
mime: "video/x-msvideo"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x30 && buf[1] === 0x26 && buf[2] === 0xB2 && buf[3] === 0x75 && buf[4] === 0x8E && buf[5] === 0x66 && buf[6] === 0xCF && buf[7] === 0x11 && buf[8] === 0xA6 && buf[9] === 0xD9) {
|
||||
return {
|
||||
ext: "wmv",
|
||||
mime: "video/x-ms-wmv"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x0 && buf[1] === 0x0 && buf[2] === 0x1 && buf[3].toString(16)[0] === "b") {
|
||||
return {
|
||||
ext: "mpg",
|
||||
mime: "video/mpeg"
|
||||
};
|
||||
}
|
||||
|
||||
if ((buf[0] === 0x49 && buf[1] === 0x44 && buf[2] === 0x33) || (buf[0] === 0xFF && buf[1] === 0xfb)) {
|
||||
return {
|
||||
ext: "mp3",
|
||||
mime: "audio/mpeg"
|
||||
};
|
||||
}
|
||||
|
||||
if ((buf[4] === 0x66 && buf[5] === 0x74 && buf[6] === 0x79 && buf[7] === 0x70 && buf[8] === 0x4D && buf[9] === 0x34 && buf[10] === 0x41) || (buf[0] === 0x4D && buf[1] === 0x34 && buf[2] === 0x41 && buf[3] === 0x20)) {
|
||||
return {
|
||||
ext: "m4a",
|
||||
mime: "audio/m4a"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x4F && buf[1] === 0x67 && buf[2] === 0x67 && buf[3] === 0x53) {
|
||||
return {
|
||||
ext: "ogg",
|
||||
mime: "audio/ogg"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x66 && buf[1] === 0x4C && buf[2] === 0x61 && buf[3] === 0x43) {
|
||||
return {
|
||||
ext: "flac",
|
||||
mime: "audio/x-flac"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x52 && buf[1] === 0x49 && buf[2] === 0x46 && buf[3] === 0x46 && buf[8] === 0x57 && buf[9] === 0x41 && buf[10] === 0x56 && buf[11] === 0x45) {
|
||||
return {
|
||||
ext: "wav",
|
||||
mime: "audio/x-wav"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x23 && buf[1] === 0x21 && buf[2] === 0x41 && buf[3] === 0x4D && buf[4] === 0x52 && buf[5] === 0x0A) {
|
||||
return {
|
||||
ext: "amr",
|
||||
mime: "audio/amr"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x25 && buf[1] === 0x50 && buf[2] === 0x44 && buf[3] === 0x46) {
|
||||
return {
|
||||
ext: "pdf",
|
||||
mime: "application/pdf"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x4D && buf[1] === 0x5A) {
|
||||
return {
|
||||
ext: "exe",
|
||||
mime: "application/x-msdownload"
|
||||
};
|
||||
}
|
||||
|
||||
if ((buf[0] === 0x43 || buf[0] === 0x46) && buf[1] === 0x57 && buf[2] === 0x53) {
|
||||
return {
|
||||
ext: "swf",
|
||||
mime: "application/x-shockwave-flash"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x7B && buf[1] === 0x5C && buf[2] === 0x72 && buf[3] === 0x74 && buf[4] === 0x66) {
|
||||
return {
|
||||
ext: "rtf",
|
||||
mime: "application/rtf"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x77 && buf[1] === 0x4F && buf[2] === 0x46 && buf[3] === 0x46 && buf[4] === 0x00 && buf[5] === 0x01 && buf[6] === 0x00 && buf[7] === 0x00) {
|
||||
return {
|
||||
ext: "woff",
|
||||
mime: "application/font-woff"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x77 && buf[1] === 0x4F && buf[2] === 0x46 && buf[3] === 0x32 && buf[4] === 0x00 && buf[5] === 0x01 && buf[6] === 0x00 && buf[7] === 0x00) {
|
||||
return {
|
||||
ext: "woff2",
|
||||
mime: "application/font-woff"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[34] === 0x4C && buf[35] === 0x50 && ((buf[8] === 0x02 && buf[9] === 0x00 && buf[10] === 0x01) || (buf[8] === 0x01 && buf[9] === 0x00 && buf[10] === 0x00) || (buf[8] === 0x02 && buf[9] === 0x00 && buf[10] === 0x02))) {
|
||||
return {
|
||||
ext: "eot",
|
||||
mime: "application/octet-stream"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x00 && buf[1] === 0x01 && buf[2] === 0x00 && buf[3] === 0x00 && buf[4] === 0x00) {
|
||||
return {
|
||||
ext: "ttf",
|
||||
mime: "application/font-sfnt"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x4F && buf[1] === 0x54 && buf[2] === 0x54 && buf[3] === 0x4F && buf[4] === 0x00) {
|
||||
return {
|
||||
ext: "otf",
|
||||
mime: "application/font-sfnt"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x00 && buf[1] === 0x00 && buf[2] === 0x01 && buf[3] === 0x00) {
|
||||
return {
|
||||
ext: "ico",
|
||||
mime: "image/x-icon"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x46 && buf[1] === 0x4C && buf[2] === 0x56 && buf[3] === 0x01) {
|
||||
return {
|
||||
ext: "flv",
|
||||
mime: "video/x-flv"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x25 && buf[1] === 0x21) {
|
||||
return {
|
||||
ext: "ps",
|
||||
mime: "application/postscript"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0xFD && buf[1] === 0x37 && buf[2] === 0x7A && buf[3] === 0x58 && buf[4] === 0x5A && buf[5] === 0x00) {
|
||||
return {
|
||||
ext: "xz",
|
||||
mime: "application/x-xz"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x53 && buf[1] === 0x51 && buf[2] === 0x4C && buf[3] === 0x69) {
|
||||
return {
|
||||
ext: "sqlite",
|
||||
mime: "application/x-sqlite3"
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Added by n1474335 [n1474335@gmail.com] from here on
|
||||
*
|
||||
*/
|
||||
if ((buf[0] === 0x1F && buf[1] === 0x9D) || (buf[0] === 0x1F && buf[1] === 0xA0)) {
|
||||
return {
|
||||
ext: "z, tar.z",
|
||||
mime: "application/x-gtar"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x7F && buf[1] === 0x45 && buf[2] === 0x4C && buf[3] === 0x46) {
|
||||
return {
|
||||
ext: "none, axf, bin, elf, o, prx, puff, so",
|
||||
mime: "application/x-executable",
|
||||
desc: "Executable and Linkable Format file. No standard file extension."
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0xCA && buf[1] === 0xFE && buf[2] === 0xBA && buf[3] === 0xBE) {
|
||||
return {
|
||||
ext: "class",
|
||||
mime: "application/java-vm"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0xEF && buf[1] === 0xBB && buf[2] === 0xBF) {
|
||||
return {
|
||||
ext: "txt",
|
||||
mime: "text/plain",
|
||||
desc: "UTF-8 encoded Unicode byte order mark detected, commonly but not exclusively seen in text files."
|
||||
};
|
||||
}
|
||||
|
||||
// Must be before Little-endian UTF-16 BOM
|
||||
if (buf[0] === 0xFF && buf[1] === 0xFE && buf[2] === 0x00 && buf[3] === 0x00) {
|
||||
return {
|
||||
ext: "UTF32LE",
|
||||
mime: "charset/utf32le",
|
||||
desc: "Little-endian UTF-32 encoded Unicode byte order mark detected."
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0xFF && buf[1] === 0xFE) {
|
||||
return {
|
||||
ext: "UTF16LE",
|
||||
mime: "charset/utf16le",
|
||||
desc: "Little-endian UTF-16 encoded Unicode byte order mark detected."
|
||||
};
|
||||
}
|
||||
|
||||
if ((buf[0x8001] === 0x43 && buf[0x8002] === 0x44 && buf[0x8003] === 0x30 && buf[0x8004] === 0x30 && buf[0x8005] === 0x31) ||
|
||||
(buf[0x8801] === 0x43 && buf[0x8802] === 0x44 && buf[0x8803] === 0x30 && buf[0x8804] === 0x30 && buf[0x8805] === 0x31) ||
|
||||
(buf[0x9001] === 0x43 && buf[0x9002] === 0x44 && buf[0x9003] === 0x30 && buf[0x9004] === 0x30 && buf[0x9005] === 0x31)) {
|
||||
return {
|
||||
ext: "iso",
|
||||
mime: "application/octet-stream",
|
||||
desc: "ISO 9660 CD/DVD image file"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0xD0 && buf[1] === 0xCF && buf[2] === 0x11 && buf[3] === 0xE0 && buf[4] === 0xA1 && buf[5] === 0xB1 && buf[6] === 0x1A && buf[7] === 0xE1) {
|
||||
return {
|
||||
ext: "doc, xls, ppt",
|
||||
mime: "application/msword, application/vnd.ms-excel, application/vnd.ms-powerpoint",
|
||||
desc: "Microsoft Office documents"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x64 && buf[1] === 0x65 && buf[2] === 0x78 && buf[3] === 0x0A && buf[4] === 0x30 && buf[5] === 0x33 && buf[6] === 0x35 && buf[7] === 0x00) {
|
||||
return {
|
||||
ext: "dex",
|
||||
mime: "application/octet-stream",
|
||||
desc: "Dalvik Executable (Android)"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x4B && buf[1] === 0x44 && buf[2] === 0x4D) {
|
||||
return {
|
||||
ext: "vmdk",
|
||||
mime: "application/vmdk, application/x-virtualbox-vmdk"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x43 && buf[1] === 0x72 && buf[2] === 0x32 && buf[3] === 0x34) {
|
||||
return {
|
||||
ext: "crx",
|
||||
mime: "application/crx",
|
||||
desc: "Google Chrome extension or packaged app"
|
||||
};
|
||||
}
|
||||
|
||||
if (buf[0] === 0x78 && (buf[1] === 0x01 || buf[1] === 0x9C || buf[1] === 0xDA || buf[1] === 0x5e)) {
|
||||
return {
|
||||
ext: "zlib",
|
||||
mime: "application/x-deflate"
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Byte frequencies of various languages generated from Wikipedia dumps taken in late 2017 and early 2018.
|
||||
* The Chi-Squared test cannot accept expected values of 0, so 0.0001 has been used to account for bytes
|
||||
|
||||
285
src/core/lib/Protobuf.mjs
Normal file
285
src/core/lib/Protobuf.mjs
Normal file
@@ -0,0 +1,285 @@
|
||||
import Utils from "../Utils";
|
||||
|
||||
/**
|
||||
* Protobuf lib. Contains functions to decode protobuf serialised
|
||||
* data without a schema or .proto file.
|
||||
*
|
||||
* Provides utility functions to encode and decode variable length
|
||||
* integers (varint).
|
||||
*
|
||||
* @author GCHQ Contributor [3]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
class Protobuf {
|
||||
|
||||
/**
|
||||
* Protobuf constructor
|
||||
*
|
||||
* @param {byteArray} data
|
||||
*/
|
||||
constructor(data) {
|
||||
// Check we have a byteArray
|
||||
if (data instanceof Array) {
|
||||
this.data = data;
|
||||
} else {
|
||||
throw new Error("Protobuf input must be a byteArray");
|
||||
}
|
||||
|
||||
// Set up masks
|
||||
this.TYPE = 0x07;
|
||||
this.NUMBER = 0x78;
|
||||
this.MSB = 0x80;
|
||||
this.VALUE = 0x7f;
|
||||
|
||||
// Declare offset and length
|
||||
this.offset = 0;
|
||||
this.LENGTH = data.length;
|
||||
}
|
||||
|
||||
// Public Functions
|
||||
|
||||
/**
|
||||
* Encode a varint from a number
|
||||
*
|
||||
* @param {number} number
|
||||
* @returns {byteArray}
|
||||
*/
|
||||
static varIntEncode(number) {
|
||||
const MSB = 0x80,
|
||||
VALUE = 0x7f,
|
||||
MSBALL = ~VALUE,
|
||||
INT = Math.pow(2, 31);
|
||||
const out = [];
|
||||
let offset = 0;
|
||||
|
||||
while (number >= INT) {
|
||||
out[offset++] = (number & 0xff) | MSB;
|
||||
number /= 128;
|
||||
}
|
||||
while (number & MSBALL) {
|
||||
out[offset++] = (number & 0xff) | MSB;
|
||||
number >>>= 7;
|
||||
}
|
||||
out[offset] = number | 0;
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a varint from the byteArray
|
||||
*
|
||||
* @param {byteArray} input
|
||||
* @returns {number}
|
||||
*/
|
||||
static varIntDecode(input) {
|
||||
const pb = new Protobuf(input);
|
||||
return pb._varInt();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse Protobuf data
|
||||
*
|
||||
* @param {byteArray} input
|
||||
* @returns {Object}
|
||||
*/
|
||||
static decode(input) {
|
||||
const pb = new Protobuf(input);
|
||||
return pb._parse();
|
||||
}
|
||||
|
||||
// Private Class Functions
|
||||
|
||||
/**
|
||||
* Main private parsing function
|
||||
*
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
_parse() {
|
||||
let object = {};
|
||||
// Continue reading whilst we still have data
|
||||
while (this.offset < this.LENGTH) {
|
||||
const field = this._parseField();
|
||||
object = this._addField(field, object);
|
||||
}
|
||||
// Throw an error if we have gone beyond the end of the data
|
||||
if (this.offset > this.LENGTH) {
|
||||
throw new Error("Exhausted Buffer");
|
||||
}
|
||||
return object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a field read from the protobuf data into the Object. As
|
||||
* protobuf fields can appear multiple times, if the field already
|
||||
* exists we need to add the new field into an array of fields
|
||||
* for that key.
|
||||
*
|
||||
* @private
|
||||
* @param {Object} field
|
||||
* @param {Object} object
|
||||
* @returns {Object}
|
||||
*/
|
||||
_addField(field, object) {
|
||||
// Get the field key/values
|
||||
const key = field.key;
|
||||
const value = field.value;
|
||||
object[key] = object.hasOwnProperty(key) ?
|
||||
object[key] instanceof Array ?
|
||||
object[key].concat([value]) :
|
||||
[object[key], value] :
|
||||
value;
|
||||
return object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a field and return the Object read from the record
|
||||
*
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
_parseField() {
|
||||
// Get the field headers
|
||||
const header = this._fieldHeader();
|
||||
const type = header.type;
|
||||
const key = header.key;
|
||||
switch (type) {
|
||||
// varint
|
||||
case 0:
|
||||
return { "key": key, "value": this._varInt() };
|
||||
// fixed 64
|
||||
case 1:
|
||||
return { "key": key, "value": this._uint64() };
|
||||
// length delimited
|
||||
case 2:
|
||||
return { "key": key, "value": this._lenDelim() };
|
||||
// fixed 32
|
||||
case 5:
|
||||
return { "key": key, "value": this._uint32() };
|
||||
// unknown type
|
||||
default:
|
||||
throw new Error("Unknown type 0x" + type.toString(16));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the field header and return the type and key
|
||||
*
|
||||
* @private
|
||||
* @returns {Object}
|
||||
*/
|
||||
_fieldHeader() {
|
||||
// Make sure we call type then number to preserve offset
|
||||
return { "type": this._fieldType(), "key": this._fieldNumber() };
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the field type from the field header. Type is stored in the
|
||||
* lower 3 bits of the tag byte. This does not move the offset on as
|
||||
* we need to read the field number from the tag byte too.
|
||||
*
|
||||
* @private
|
||||
* @returns {number}
|
||||
*/
|
||||
_fieldType() {
|
||||
// Field type stored in lower 3 bits of tag byte
|
||||
return this.data[this.offset] & this.TYPE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the field number (i.e. the key) from the field header. The
|
||||
* field number is stored in the upper 5 bits of the tag byte - but
|
||||
* is also varint encoded so the follow on bytes may need to be read
|
||||
* when field numbers are > 15.
|
||||
*
|
||||
* @private
|
||||
* @returns {number}
|
||||
*/
|
||||
_fieldNumber() {
|
||||
let shift = -3;
|
||||
let fieldNumber = 0;
|
||||
do {
|
||||
fieldNumber += shift < 28 ?
|
||||
shift === -3 ?
|
||||
(this.data[this.offset] & this.NUMBER) >> -shift :
|
||||
(this.data[this.offset] & this.VALUE) << shift :
|
||||
(this.data[this.offset] & this.VALUE) * Math.pow(2, shift);
|
||||
shift += 7;
|
||||
} while ((this.data[this.offset++] & this.MSD) === this.MSB);
|
||||
return fieldNumber;
|
||||
}
|
||||
|
||||
// Field Parsing Functions
|
||||
|
||||
/**
|
||||
* Read off a varint from the data
|
||||
*
|
||||
* @private
|
||||
* @returns {number}
|
||||
*/
|
||||
_varInt() {
|
||||
let value = 0;
|
||||
let shift = 0;
|
||||
// Keep reading while upper bit set
|
||||
do {
|
||||
value += shift < 28 ?
|
||||
(this.data[this.offset] & this.VALUE) << shift :
|
||||
(this.data[this.offset] & this.VALUE) * Math.pow(2, shift);
|
||||
shift += 7;
|
||||
} while ((this.data[this.offset++] & this.MSB) === this.MSB);
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read off a 64 bit unsigned integer from the data
|
||||
*
|
||||
* @private
|
||||
* @returns {number}
|
||||
*/
|
||||
_uint64() {
|
||||
// Read off a Uint64
|
||||
let num = this.data[this.offset++] * 0x1000000 + (this.data[this.offset++] << 16) + (this.data[this.offset++] << 8) + this.data[this.offset++];
|
||||
num = num * 0x100000000 + this.data[this.offset++] * 0x1000000 + (this.data[this.offset++] << 16) + (this.data[this.offset++] << 8) + this.data[this.offset++];
|
||||
return num;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read off a length delimited field from the data
|
||||
*
|
||||
* @private
|
||||
* @returns {Object|string}
|
||||
*/
|
||||
_lenDelim() {
|
||||
// Read off the field length
|
||||
const length = this._varInt();
|
||||
const fieldBytes = this.data.slice(this.offset, this.offset + length);
|
||||
let field;
|
||||
try {
|
||||
// Attempt to parse as a new Protobuf Object
|
||||
const pbObject = new Protobuf(fieldBytes);
|
||||
field = pbObject._parse();
|
||||
} catch (err) {
|
||||
// Otherwise treat as bytes
|
||||
field = Utils.byteArrayToChars(fieldBytes);
|
||||
}
|
||||
// Move the offset and return the field
|
||||
this.offset += length;
|
||||
return field;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a 32 bit unsigned integer from the data
|
||||
*
|
||||
* @private
|
||||
* @returns {number}
|
||||
*/
|
||||
_uint32() {
|
||||
// Use a dataview to read off the integer
|
||||
const dataview = new DataView(new Uint8Array(this.data.slice(this.offset, this.offset + 4)).buffer);
|
||||
const value = dataview.getUint32(0);
|
||||
this.offset += 4;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
export default Protobuf;
|
||||
263
src/core/lib/Stream.mjs
Normal file
263
src/core/lib/Stream.mjs
Normal file
@@ -0,0 +1,263 @@
|
||||
/**
|
||||
* Stream class for parsing binary protocols.
|
||||
*
|
||||
* @author n1474335 [n1474335@gmail.com]
|
||||
* @author tlwr [toby@toby.codes]
|
||||
* @copyright Crown Copyright 2018
|
||||
* @license Apache-2.0
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* A Stream can be used to traverse a binary blob, interpreting sections of it
|
||||
* as various data types.
|
||||
*/
|
||||
export default class Stream {
|
||||
|
||||
/**
|
||||
* Stream constructor.
|
||||
*
|
||||
* @param {Uint8Array} input
|
||||
*/
|
||||
constructor(input) {
|
||||
this.bytes = input;
|
||||
this.length = this.bytes.length;
|
||||
this.position = 0;
|
||||
this.bitPos = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a number of bytes from the current position.
|
||||
*
|
||||
* @param {number} numBytes
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
getBytes(numBytes) {
|
||||
if (this.position > this.length) return undefined;
|
||||
|
||||
const newPosition = this.position + numBytes;
|
||||
const bytes = this.bytes.slice(this.position, newPosition);
|
||||
this.position = newPosition;
|
||||
this.bitPos = 0;
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpret the following bytes as a string, stopping at the next null byte or
|
||||
* the supplied limit.
|
||||
*
|
||||
* @param {number} numBytes
|
||||
* @returns {string}
|
||||
*/
|
||||
readString(numBytes) {
|
||||
if (this.position > this.length) return undefined;
|
||||
|
||||
let result = "";
|
||||
for (let i = this.position; i < this.position + numBytes; i++) {
|
||||
const currentByte = this.bytes[i];
|
||||
if (currentByte === 0) break;
|
||||
result += String.fromCharCode(currentByte);
|
||||
}
|
||||
this.position += numBytes;
|
||||
this.bitPos = 0;
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Interpret the following bytes as an integer in big or little endian.
|
||||
*
|
||||
* @param {number} numBytes
|
||||
* @param {string} [endianness="be"]
|
||||
* @returns {number}
|
||||
*/
|
||||
readInt(numBytes, endianness="be") {
|
||||
if (this.position > this.length) return undefined;
|
||||
|
||||
let val = 0;
|
||||
if (endianness === "be") {
|
||||
for (let i = this.position; i < this.position + numBytes; i++) {
|
||||
val = val << 8;
|
||||
val |= this.bytes[i];
|
||||
}
|
||||
} else {
|
||||
for (let i = this.position + numBytes - 1; i >= this.position; i--) {
|
||||
val = val << 8;
|
||||
val |= this.bytes[i];
|
||||
}
|
||||
}
|
||||
this.position += numBytes;
|
||||
this.bitPos = 0;
|
||||
return val;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a number of bits from the buffer.
|
||||
*
|
||||
* @TODO Add endianness
|
||||
*
|
||||
* @param {number} numBits
|
||||
* @returns {number}
|
||||
*/
|
||||
readBits(numBits) {
|
||||
if (this.position > this.length) return undefined;
|
||||
|
||||
let bitBuf = 0,
|
||||
bitBufLen = 0;
|
||||
|
||||
// Add remaining bits from current byte
|
||||
bitBuf = (this.bytes[this.position++] & bitMask(this.bitPos)) >>> this.bitPos;
|
||||
bitBufLen = 8 - this.bitPos;
|
||||
this.bitPos = 0;
|
||||
|
||||
// Not enough bits yet
|
||||
while (bitBufLen < numBits) {
|
||||
bitBuf |= this.bytes[this.position++] << bitBufLen;
|
||||
bitBufLen += 8;
|
||||
}
|
||||
|
||||
// Reverse back to numBits
|
||||
if (bitBufLen > numBits) {
|
||||
const excess = bitBufLen - numBits;
|
||||
bitBuf &= (1 << numBits) - 1;
|
||||
bitBufLen -= excess;
|
||||
this.position--;
|
||||
this.bitPos = 8 - excess;
|
||||
}
|
||||
|
||||
return bitBuf;
|
||||
|
||||
/**
|
||||
* Calculates the bit mask based on the current bit position.
|
||||
*
|
||||
* @param {number} bitPos
|
||||
* @returns {number} The bit mask
|
||||
*/
|
||||
function bitMask(bitPos) {
|
||||
return 256 - (1 << bitPos);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume the stream until we reach the specified byte or sequence of bytes.
|
||||
*
|
||||
* @param {number|List<number>} val
|
||||
*/
|
||||
continueUntil(val) {
|
||||
if (this.position > this.length) return;
|
||||
|
||||
this.bitPos = 0;
|
||||
|
||||
if (typeof val === "number") {
|
||||
while (++this.position < this.length && this.bytes[this.position] !== val) {
|
||||
continue;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// val is an array
|
||||
let found = false;
|
||||
while (!found && this.position < this.length) {
|
||||
while (++this.position < this.length && this.bytes[this.position] !== val[0]) {
|
||||
continue;
|
||||
}
|
||||
found = true;
|
||||
for (let i = 1; i < val.length; i++) {
|
||||
if (this.position + i > this.length || this.bytes[this.position + i] !== val[i])
|
||||
found = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Consume the next byte if it matches the supplied value.
|
||||
*
|
||||
* @param {number} val
|
||||
*/
|
||||
consumeIf(val) {
|
||||
if (this.bytes[this.position] === val) {
|
||||
this.position++;
|
||||
this.bitPos = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move forwards through the stream by the specified number of bytes.
|
||||
*
|
||||
* @param {number} numBytes
|
||||
*/
|
||||
moveForwardsBy(numBytes) {
|
||||
const pos = this.position + numBytes;
|
||||
if (pos < 0 || pos > this.length)
|
||||
throw new Error("Cannot move to position " + pos + " in stream. Out of bounds.");
|
||||
this.position = pos;
|
||||
this.bitPos = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move backwards through the stream by the specified number of bytes.
|
||||
*
|
||||
* @param {number} numBytes
|
||||
*/
|
||||
moveBackwardsBy(numBytes) {
|
||||
const pos = this.position - numBytes;
|
||||
if (pos < 0 || pos > this.length)
|
||||
throw new Error("Cannot move to position " + pos + " in stream. Out of bounds.");
|
||||
this.position = pos;
|
||||
this.bitPos = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move backwards through the strem by the specified number of bits.
|
||||
*
|
||||
* @param {number} numBits
|
||||
*/
|
||||
moveBackwardsByBits(numBits) {
|
||||
if (numBits <= this.bitPos) {
|
||||
this.bitPos -= numBits;
|
||||
} else {
|
||||
if (this.bitPos > 0) {
|
||||
numBits -= this.bitPos;
|
||||
this.bitPos = 0;
|
||||
}
|
||||
|
||||
while (numBits > 0) {
|
||||
this.moveBackwardsBy(1);
|
||||
this.bitPos = 8;
|
||||
this.moveBackwardsByBits(numBits);
|
||||
numBits -= 8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move to a specified position in the stream.
|
||||
*
|
||||
* @param {number} pos
|
||||
*/
|
||||
moveTo(pos) {
|
||||
if (pos < 0 || pos > this.length)
|
||||
throw new Error("Cannot move to position " + pos + " in stream. Out of bounds.");
|
||||
this.position = pos;
|
||||
this.bitPos = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if there are more bytes left in the stream.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasMore() {
|
||||
return this.position < this.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a slice of the stream up to the current position.
|
||||
*
|
||||
* @returns {Uint8Array}
|
||||
*/
|
||||
carve() {
|
||||
if (this.bitPos > 0) this.position++;
|
||||
return this.bytes.slice(0, this.position);
|
||||
}
|
||||
|
||||
}
|
||||
227
src/core/lib/Typex.mjs
Normal file
227
src/core/lib/Typex.mjs
Normal file
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* Emulation of the Typex machine.
|
||||
*
|
||||
* @author s2224834
|
||||
* @author The National Museum of Computing - Bombe Rebuild Project
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
import OperationError from "../errors/OperationError";
|
||||
import * as Enigma from "../lib/Enigma";
|
||||
import Utils from "../Utils";
|
||||
|
||||
/**
|
||||
* A set of example Typex rotors. No Typex rotor wirings are publicly available, so these are
|
||||
* randomised.
|
||||
*/
|
||||
export const ROTORS = [
|
||||
{name: "Example 1", value: "MCYLPQUVRXGSAOWNBJEZDTFKHI<BFHNQUW"},
|
||||
{name: "Example 2", value: "KHWENRCBISXJQGOFMAPVYZDLTU<BFHNQUW"},
|
||||
{name: "Example 3", value: "BYPDZMGIKQCUSATREHOJNLFWXV<BFHNQUW"},
|
||||
{name: "Example 4", value: "ZANJCGDLVHIXOBRPMSWQUKFYET<BFHNQUW"},
|
||||
{name: "Example 5", value: "QXBGUTOVFCZPJIHSWERYNDAMLK<BFHNQUW"},
|
||||
{name: "Example 6", value: "BDCNWUEIQVFTSXALOGZJYMHKPR<BFHNQUW"},
|
||||
{name: "Example 7", value: "WJUKEIABMSGFTQZVCNPHORDXYL<BFHNQUW"},
|
||||
{name: "Example 8", value: "TNVCZXDIPFWQKHSJMAOYLEURGB<BFHNQUW"},
|
||||
];
|
||||
|
||||
/**
|
||||
* An example Typex reflector. Again, randomised.
|
||||
*/
|
||||
export const REFLECTORS = [
|
||||
{name: "Example", value: "AN BC FG IE KD LU MH OR TS VZ WQ XJ YP"},
|
||||
];
|
||||
|
||||
// Special character handling on Typex keyboard
|
||||
const KEYBOARD = {
|
||||
"Q": "1", "W": "2", "E": "3", "R": "4", "T": "5", "Y": "6", "U": "7", "I": "8", "O": "9", "P": "0",
|
||||
"A": "-", "S": "/", "D": "Z", "F": "%", "G": "X", "H": "£", "K": "(", "L": ")",
|
||||
"C": "V", "B": "'", "N": ",", "M": "."
|
||||
};
|
||||
const KEYBOARD_REV = {};
|
||||
for (const i of Object.keys(KEYBOARD)) {
|
||||
KEYBOARD_REV[KEYBOARD[i]] = i;
|
||||
}
|
||||
|
||||
/**
|
||||
* Typex machine. A lot like the Enigma, but five rotors, of which the first two are static.
|
||||
*/
|
||||
export class TypexMachine extends Enigma.EnigmaBase {
|
||||
/**
|
||||
* TypexMachine constructor.
|
||||
*
|
||||
* @param {Object[]} rotors - List of Rotors.
|
||||
* @param {Object} reflector - A Reflector.
|
||||
* @param {Plugboard} plugboard - A Plugboard.
|
||||
*/
|
||||
constructor(rotors, reflector, plugboard, keyboard) {
|
||||
super(rotors, reflector, plugboard);
|
||||
if (rotors.length !== 5) {
|
||||
throw new OperationError("Typex must have 5 rotors");
|
||||
}
|
||||
this.keyboard = keyboard;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the same as the Enigma step function, it's just that the right-
|
||||
* most two rotors are static.
|
||||
*/
|
||||
step() {
|
||||
const r0 = this.rotors[2];
|
||||
const r1 = this.rotors[3];
|
||||
r0.step();
|
||||
// The second test here is the double-stepping anomaly
|
||||
if (r0.steps.has(r0.pos) || r1.steps.has(Utils.mod(r1.pos + 1, 26))) {
|
||||
r1.step();
|
||||
if (r1.steps.has(r1.pos)) {
|
||||
const r2 = this.rotors[4];
|
||||
r2.step();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt/decrypt data. This is identical to the Enigma version cryptographically, but we have
|
||||
* additional handling for the Typex's keyboard (which handles various special characters by
|
||||
* mapping them to particular letter combinations).
|
||||
*
|
||||
* @param {string} input - The data to encrypt/decrypt.
|
||||
* @return {string}
|
||||
*/
|
||||
crypt(input) {
|
||||
let inputMod = input;
|
||||
if (this.keyboard === "Encrypt") {
|
||||
inputMod = "";
|
||||
// true = in symbol mode
|
||||
let mode = false;
|
||||
for (const x of input) {
|
||||
if (x === " ") {
|
||||
inputMod += "X";
|
||||
} else if (mode) {
|
||||
if (KEYBOARD_REV.hasOwnProperty(x)) {
|
||||
inputMod += KEYBOARD_REV[x];
|
||||
} else {
|
||||
mode = false;
|
||||
inputMod += "V" + x;
|
||||
}
|
||||
} else {
|
||||
if (KEYBOARD_REV.hasOwnProperty(x)) {
|
||||
mode = true;
|
||||
inputMod += "Z" + KEYBOARD_REV[x];
|
||||
} else {
|
||||
inputMod += x;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const output = super.crypt(inputMod);
|
||||
|
||||
let outputMod = output;
|
||||
if (this.keyboard === "Decrypt") {
|
||||
outputMod = "";
|
||||
let mode = false;
|
||||
for (const x of output) {
|
||||
if (x === "X") {
|
||||
outputMod += " ";
|
||||
} else if (x === "V") {
|
||||
mode = false;
|
||||
} else if (x === "Z") {
|
||||
mode = true;
|
||||
} else if (mode) {
|
||||
outputMod += KEYBOARD[x];
|
||||
} else {
|
||||
outputMod += x;
|
||||
}
|
||||
}
|
||||
}
|
||||
return outputMod;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Typex rotor. Like an Enigma rotor, but no ring setting, and can be reversed.
|
||||
*/
|
||||
export class Rotor extends Enigma.Rotor {
|
||||
/**
|
||||
* Rotor constructor.
|
||||
*
|
||||
* @param {string} wiring - A 26 character string of the wiring order.
|
||||
* @param {string} steps - A 0..26 character string of stepping points.
|
||||
* @param {bool} reversed - Whether to reverse the rotor.
|
||||
* @param {char} ringSetting - Ring setting of the rotor.
|
||||
* @param {char} initialPosition - The initial position of the rotor.
|
||||
*/
|
||||
constructor(wiring, steps, reversed, ringSetting, initialPos) {
|
||||
let wiringMod = wiring;
|
||||
if (reversed) {
|
||||
const outMap = new Array(26);
|
||||
for (let i=0; i<26; i++) {
|
||||
// wiring[i] is the original output
|
||||
// Enigma.LETTERS[i] is the original input
|
||||
const input = Utils.mod(26 - Enigma.a2i(wiring[i]), 26);
|
||||
const output = Enigma.i2a(Utils.mod(26 - Enigma.a2i(Enigma.LETTERS[i]), 26));
|
||||
outMap[input] = output;
|
||||
}
|
||||
wiringMod = outMap.join("");
|
||||
}
|
||||
super(wiringMod, steps, ringSetting, initialPos);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Typex input plugboard. Based on a Rotor, because it allows arbitrary maps, not just switches
|
||||
* like the Enigma plugboard.
|
||||
* Not to be confused with the reflector plugboard.
|
||||
* This is also where the Typex's backwards input wiring is implemented - it's a bit of a hack, but
|
||||
* it means everything else continues to work like in the Enigma.
|
||||
*/
|
||||
export class Plugboard extends Enigma.Rotor {
|
||||
/**
|
||||
* Typex plugboard constructor.
|
||||
*
|
||||
* @param {string} wiring - 26 character string of mappings from A-Z, as per rotors, or "".
|
||||
*/
|
||||
constructor(wiring) {
|
||||
// Typex input wiring is backwards vs Enigma: that is, letters enter the rotors in a
|
||||
// clockwise order, vs. Enigma's anticlockwise (or vice versa depending on which side
|
||||
// you're looking at it from). I'm doing the transform here to avoid having to rewrite
|
||||
// the Engima crypt() method in Typex as well.
|
||||
// Note that the wiring for the reflector is the same way around as Enigma, so no
|
||||
// transformation is necessary on that side.
|
||||
// We're going to achieve this by mapping the plugboard settings through an additional
|
||||
// transform that mirrors the alphabet before we pass it to the superclass.
|
||||
if (!/^[A-Z]{26}$/.test(wiring)) {
|
||||
throw new OperationError("Plugboard wiring must be 26 unique uppercase letters");
|
||||
}
|
||||
const reversed = "AZYXWVUTSRQPONMLKJIHGFEDCB";
|
||||
wiring = wiring.replace(/./g, x => {
|
||||
return reversed[Enigma.a2i(x)];
|
||||
});
|
||||
try {
|
||||
super(wiring, "", "A", "A");
|
||||
} catch (err) {
|
||||
throw new OperationError(err.message.replace("Rotor", "Plugboard"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a character through this rotor forwards.
|
||||
*
|
||||
* @param {number} c - The character.
|
||||
* @returns {number}
|
||||
*/
|
||||
transform(c) {
|
||||
return Utils.mod(this.map[Utils.mod(c + this.pos, 26)] - this.pos, 26);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a character through this rotor backwards.
|
||||
*
|
||||
* @param {number} c - The character.
|
||||
* @returns {number}
|
||||
*/
|
||||
revTransform(c) {
|
||||
return Utils.mod(this.revMap[Utils.mod(c + this.pos, 26)] - this.pos, 26);
|
||||
}
|
||||
}
|
||||
@@ -65,8 +65,8 @@ class AESEncrypt extends Operation {
|
||||
* @throws {OperationError} if invalid key length
|
||||
*/
|
||||
run(input, args) {
|
||||
const key = Utils.convertToByteArray(args[0].string, args[0].option),
|
||||
iv = Utils.convertToByteArray(args[1].string, args[1].option),
|
||||
const key = Utils.convertToByteString(args[0].string, args[0].option),
|
||||
iv = Utils.convertToByteString(args[1].string, args[1].option),
|
||||
mode = args[2],
|
||||
inputType = args[3],
|
||||
outputType = args[4];
|
||||
|
||||
79
src/core/operations/BLAKE2b.mjs
Normal file
79
src/core/operations/BLAKE2b.mjs
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* @author h345983745
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import blakejs from "blakejs";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import Utils from "../Utils";
|
||||
import { toBase64 } from "../lib/Base64";
|
||||
|
||||
/**
|
||||
* BLAKE2b operation
|
||||
*/
|
||||
class BLAKE2b extends Operation {
|
||||
|
||||
/**
|
||||
* BLAKE2b constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "BLAKE2b";
|
||||
this.module = "Hashing";
|
||||
this.description = `Performs BLAKE2b hashing on the input.
|
||||
<br><br> BLAKE2b is a flavour of the BLAKE cryptographic hash function that is optimized for 64-bit platforms and produces digests of any size between 1 and 64 bytes.
|
||||
<br><br> Supports the use of an optional key.`;
|
||||
this.infoURL = "https://wikipedia.org/wiki/BLAKE_(hash_function)#BLAKE2b_algorithm";
|
||||
this.inputType = "ArrayBuffer";
|
||||
this.outputType = "string";
|
||||
this.args = [
|
||||
{
|
||||
"name": "Size",
|
||||
"type": "option",
|
||||
"value": ["512", "384", "256", "160", "128"]
|
||||
}, {
|
||||
"name": "Output Encoding",
|
||||
"type": "option",
|
||||
"value": ["Hex", "Base64", "Raw"]
|
||||
}, {
|
||||
"name": "Key",
|
||||
"type": "toggleString",
|
||||
"value": "",
|
||||
"toggleValues": ["UTF8", "Decimal", "Base64", "Hex", "Latin1"]
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ArrayBuffer} input
|
||||
* @param {Object[]} args
|
||||
* @returns {string} The input having been hashed with BLAKE2b in the encoding format speicifed.
|
||||
*/
|
||||
run(input, args) {
|
||||
const [outSize, outFormat] = args;
|
||||
let key = Utils.convertToByteArray(args[2].string || "", args[2].option);
|
||||
if (key.length === 0) {
|
||||
key = null;
|
||||
} else if (key.length > 64) {
|
||||
throw new OperationError(["Key cannot be greater than 64 bytes", "It is currently " + key.length + " bytes."].join("\n"));
|
||||
}
|
||||
|
||||
input = new Uint8Array(input);
|
||||
switch (outFormat) {
|
||||
case "Hex":
|
||||
return blakejs.blake2bHex(input, key, outSize / 8);
|
||||
case "Base64":
|
||||
return toBase64(blakejs.blake2b(input, key, outSize / 8));
|
||||
case "Raw":
|
||||
return Utils.arrayBufferToStr(blakejs.blake2b(input, key, outSize / 8).buffer);
|
||||
default:
|
||||
return new OperationError("Unsupported Output Type");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default BLAKE2b;
|
||||
80
src/core/operations/BLAKE2s.mjs
Normal file
80
src/core/operations/BLAKE2s.mjs
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* @author h345983745
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import blakejs from "blakejs";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import Utils from "../Utils";
|
||||
import { toBase64 } from "../lib/Base64";
|
||||
|
||||
/**
|
||||
* BLAKE2s Operation
|
||||
*/
|
||||
class BLAKE2s extends Operation {
|
||||
|
||||
/**
|
||||
* BLAKE2s constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "BLAKE2s";
|
||||
this.module = "Hashing";
|
||||
this.description = `Performs BLAKE2s hashing on the input.
|
||||
<br><br>BLAKE2s is a flavour of the BLAKE cryptographic hash function that is optimized for 8- to 32-bit platforms and produces digests of any size between 1 and 32 bytes.
|
||||
<br><br>Supports the use of an optional key.`;
|
||||
this.infoURL = "https://wikipedia.org/wiki/BLAKE_(hash_function)#BLAKE2";
|
||||
this.inputType = "ArrayBuffer";
|
||||
this.outputType = "string";
|
||||
this.args = [
|
||||
{
|
||||
"name": "Size",
|
||||
"type": "option",
|
||||
"value": ["256", "160", "128"]
|
||||
}, {
|
||||
"name": "Output Encoding",
|
||||
"type": "option",
|
||||
"value": ["Hex", "Base64", "Raw"]
|
||||
},
|
||||
{
|
||||
"name": "Key",
|
||||
"type": "toggleString",
|
||||
"value": "",
|
||||
"toggleValues": ["UTF8", "Decimal", "Base64", "Hex", "Latin1"]
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ArrayBuffer} input
|
||||
* @param {Object[]} args
|
||||
* @returns {string} The input having been hashed with BLAKE2s in the encoding format speicifed.
|
||||
*/
|
||||
run(input, args) {
|
||||
const [outSize, outFormat] = args;
|
||||
let key = Utils.convertToByteArray(args[2].string || "", args[2].option);
|
||||
if (key.length === 0) {
|
||||
key = null;
|
||||
} else if (key.length > 32) {
|
||||
throw new OperationError(["Key cannot be greater than 32 bytes", "It is currently " + key.length + " bytes."].join("\n"));
|
||||
}
|
||||
|
||||
input = new Uint8Array(input);
|
||||
switch (outFormat) {
|
||||
case "Hex":
|
||||
return blakejs.blake2sHex(input, key, outSize / 8);
|
||||
case "Base64":
|
||||
return toBase64(blakejs.blake2s(input, key, outSize / 8));
|
||||
case "Raw":
|
||||
return Utils.arrayBufferToStr(blakejs.blake2s(input, key, outSize / 8).buffer);
|
||||
default:
|
||||
return new OperationError("Unsupported Output Type");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default BLAKE2s;
|
||||
102
src/core/operations/BlurImage.mjs
Normal file
102
src/core/operations/BlurImage.mjs
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* @author j433866 [j433866@gmail.com]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import { isImage } from "../lib/FileType";
|
||||
import { toBase64 } from "../lib/Base64";
|
||||
import jimp from "jimp";
|
||||
|
||||
/**
|
||||
* Blur Image operation
|
||||
*/
|
||||
class BlurImage extends Operation {
|
||||
|
||||
/**
|
||||
* BlurImage constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Blur Image";
|
||||
this.module = "Image";
|
||||
this.description = "Applies a blur effect to the image.<br><br>Gaussian blur is much slower than fast blur, but produces better results.";
|
||||
this.infoURL = "https://wikipedia.org/wiki/Gaussian_blur";
|
||||
this.inputType = "byteArray";
|
||||
this.outputType = "byteArray";
|
||||
this.presentType = "html";
|
||||
this.args = [
|
||||
{
|
||||
name: "Amount",
|
||||
type: "number",
|
||||
value: 5,
|
||||
min: 1
|
||||
},
|
||||
{
|
||||
name: "Type",
|
||||
type: "option",
|
||||
value: ["Fast", "Gaussian"]
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {byteArray} input
|
||||
* @param {Object[]} args
|
||||
* @returns {byteArray}
|
||||
*/
|
||||
async run(input, args) {
|
||||
const [blurAmount, blurType] = args;
|
||||
|
||||
if (!isImage(input)) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
let image;
|
||||
try {
|
||||
image = await jimp.read(Buffer.from(input));
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error loading image. (${err})`);
|
||||
}
|
||||
try {
|
||||
switch (blurType){
|
||||
case "Fast":
|
||||
image.blur(blurAmount);
|
||||
break;
|
||||
case "Gaussian":
|
||||
if (ENVIRONMENT_IS_WORKER())
|
||||
self.sendStatusMessage("Gaussian blurring image. This may take a while...");
|
||||
image.gaussian(blurAmount);
|
||||
break;
|
||||
}
|
||||
|
||||
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
|
||||
return [...imageBuffer];
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error blurring image. (${err})`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the blurred image using HTML for web apps
|
||||
*
|
||||
* @param {byteArray} data
|
||||
* @returns {html}
|
||||
*/
|
||||
present(data) {
|
||||
if (!data.length) return "";
|
||||
|
||||
const type = isImage(data);
|
||||
if (!type) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
return `<img src="data:${type};base64,${toBase64(data)}">`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default BlurImage;
|
||||
175
src/core/operations/Bombe.mjs
Normal file
175
src/core/operations/Bombe.mjs
Normal file
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* Emulation of the Bombe machine.
|
||||
*
|
||||
* @author s2224834
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import {BombeMachine} from "../lib/Bombe";
|
||||
import {ROTORS, ROTORS_FOURTH, REFLECTORS, Reflector} from "../lib/Enigma";
|
||||
|
||||
/**
|
||||
* Bombe operation
|
||||
*/
|
||||
class Bombe extends Operation {
|
||||
/**
|
||||
* Bombe constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Bombe";
|
||||
this.module = "Default";
|
||||
this.description = "Emulation of the Bombe machine used at Bletchley Park to attack Enigma, based on work by Polish and British cryptanalysts.<br><br>To run this you need to have a 'crib', which is some known plaintext for a chunk of the target ciphertext, and know the rotors used. (See the 'Bombe (multiple runs)' operation if you don't know the rotors.) The machine will suggest possible configurations of the Enigma. Each suggestion has the rotor start positions (left to right) and known plugboard pairs.<br><br>Choosing a crib: First, note that Enigma cannot encrypt a letter to itself, which allows you to rule out some positions for possible cribs. Secondly, the Bombe does not simulate the Enigma's middle rotor stepping. The longer your crib, the more likely a step happened within it, which will prevent the attack working. However, other than that, longer cribs are generally better. The attack produces a 'menu' which maps ciphertext letters to plaintext, and the goal is to produce 'loops': for example, with ciphertext ABC and crib CAB, we have the mappings A<->C, B<->A, and C<->B, which produces a loop A-B-C-A. The more loops, the better the crib. The operation will output this: if your menu has too few loops or is too short, a large number of incorrect outputs will usually be produced. Try a different crib. If the menu seems good but the right answer isn't produced, your crib may be wrong, or you may have overlapped the middle rotor stepping - try a different crib.<br><br>Output is not sufficient to fully decrypt the data. You will have to recover the rest of the plugboard settings by inspection. And the ring position is not taken into account: this affects when the middle rotor steps. If your output is correct for a bit, and then goes wrong, adjust the ring and start position on the right-hand rotor together until the output improves. If necessary, repeat for the middle rotor.<br><br>By default this operation runs the checking machine, a manual process to verify the quality of Bombe stops, on each stop, discarding stops which fail. If you want to see how many times the hardware actually stops for a given input, disable the checking machine.<br><br>More detailed descriptions of the Enigma, Typex and Bombe operations <a href='https://github.com/gchq/CyberChef/wiki/Enigma,-the-Bombe,-and-Typex'>can be found here</a>.";
|
||||
this.infoURL = "https://wikipedia.org/wiki/Bombe";
|
||||
this.inputType = "string";
|
||||
this.outputType = "JSON";
|
||||
this.presentType = "html";
|
||||
this.args = [
|
||||
{
|
||||
name: "Model",
|
||||
type: "argSelector",
|
||||
value: [
|
||||
{
|
||||
name: "3-rotor",
|
||||
off: [1]
|
||||
},
|
||||
{
|
||||
name: "4-rotor",
|
||||
on: [1]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Left-most (4th) rotor",
|
||||
type: "editableOption",
|
||||
value: ROTORS_FOURTH,
|
||||
defaultIndex: 0
|
||||
},
|
||||
{
|
||||
name: "Left-hand rotor",
|
||||
type: "editableOption",
|
||||
value: ROTORS,
|
||||
defaultIndex: 0
|
||||
},
|
||||
{
|
||||
name: "Middle rotor",
|
||||
type: "editableOption",
|
||||
value: ROTORS,
|
||||
defaultIndex: 1
|
||||
},
|
||||
{
|
||||
name: "Right-hand rotor",
|
||||
type: "editableOption",
|
||||
value: ROTORS,
|
||||
defaultIndex: 2
|
||||
},
|
||||
{
|
||||
name: "Reflector",
|
||||
type: "editableOption",
|
||||
value: REFLECTORS
|
||||
},
|
||||
{
|
||||
name: "Crib",
|
||||
type: "string",
|
||||
value: ""
|
||||
},
|
||||
{
|
||||
name: "Crib offset",
|
||||
type: "number",
|
||||
value: 0
|
||||
},
|
||||
{
|
||||
name: "Use checking machine",
|
||||
type: "boolean",
|
||||
value: true
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Format and send a status update message.
|
||||
* @param {number} nLoops - Number of loops in the menu
|
||||
* @param {number} nStops - How many stops so far
|
||||
* @param {number} progress - Progress (as a float in the range 0..1)
|
||||
*/
|
||||
updateStatus(nLoops, nStops, progress) {
|
||||
const msg = `Bombe run with ${nLoops} loop${nLoops === 1 ? "" : "s"} in menu (2+ desirable): ${nStops} stops, ${Math.floor(100 * progress)}% done`;
|
||||
self.sendStatusMessage(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} input
|
||||
* @param {Object[]} args
|
||||
* @returns {string}
|
||||
*/
|
||||
run(input, args) {
|
||||
const model = args[0];
|
||||
const reflectorstr = args[5];
|
||||
let crib = args[6];
|
||||
const offset = args[7];
|
||||
const check = args[8];
|
||||
const rotors = [];
|
||||
for (let i=0; i<4; i++) {
|
||||
if (i === 0 && model === "3-rotor") {
|
||||
// No fourth rotor
|
||||
continue;
|
||||
}
|
||||
let rstr = args[i + 1];
|
||||
// The Bombe doesn't take stepping into account so we'll just ignore it here
|
||||
if (rstr.includes("<")) {
|
||||
rstr = rstr.split("<", 2)[0];
|
||||
}
|
||||
rotors.push(rstr);
|
||||
}
|
||||
// Rotors are handled in reverse
|
||||
rotors.reverse();
|
||||
if (crib.length === 0) {
|
||||
throw new OperationError("Crib cannot be empty");
|
||||
}
|
||||
if (offset < 0) {
|
||||
throw new OperationError("Offset cannot be negative");
|
||||
}
|
||||
// For symmetry with the Enigma op, for the input we'll just remove all invalid characters
|
||||
input = input.replace(/[^A-Za-z]/g, "").toUpperCase();
|
||||
crib = crib.replace(/[^A-Za-z]/g, "").toUpperCase();
|
||||
const ciphertext = input.slice(offset);
|
||||
const reflector = new Reflector(reflectorstr);
|
||||
let update;
|
||||
if (ENVIRONMENT_IS_WORKER()) {
|
||||
update = this.updateStatus;
|
||||
} else {
|
||||
update = undefined;
|
||||
}
|
||||
const bombe = new BombeMachine(rotors, reflector, ciphertext, crib, check, update);
|
||||
const result = bombe.run();
|
||||
return {
|
||||
nLoops: bombe.nLoops,
|
||||
result: result
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Displays the Bombe results in an HTML table
|
||||
*
|
||||
* @param {Object} output
|
||||
* @param {number} output.nLoops
|
||||
* @param {Array[]} output.result
|
||||
* @returns {html}
|
||||
*/
|
||||
present(output) {
|
||||
let html = `Bombe run on menu with ${output.nLoops} loop${output.nLoops === 1 ? "" : "s"} (2+ desirable). Note: Rotor positions are listed left to right and start at the beginning of the crib, and ignore stepping and the ring setting. Some plugboard settings are determined. A decryption preview starting at the beginning of the crib and ignoring stepping is also provided.\n\n`;
|
||||
html += "<table class='table table-hover table-sm table-bordered table-nonfluid'><tr><th>Rotor stops</th> <th>Partial plugboard</th> <th>Decryption preview</th></tr>\n";
|
||||
for (const [setting, stecker, decrypt] of output.result) {
|
||||
html += `<tr><td>${setting}</td> <td>${stecker}</td> <td>${decrypt}</td></tr>\n`;
|
||||
}
|
||||
html += "</table>";
|
||||
return html;
|
||||
}
|
||||
}
|
||||
|
||||
export default Bombe;
|
||||
72
src/core/operations/Bzip2Compress.mjs
Normal file
72
src/core/operations/Bzip2Compress.mjs
Normal file
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* @author Matt C [me@mitt.dev]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import Bzip2 from "libbzip2-wasm";
|
||||
|
||||
/**
|
||||
* Bzip2 Compress operation
|
||||
*/
|
||||
class Bzip2Compress extends Operation {
|
||||
|
||||
/**
|
||||
* Bzip2Compress constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Bzip2 Compress";
|
||||
this.module = "Compression";
|
||||
this.description = "Bzip2 is a compression library developed by Julian Seward (of GHC fame) that uses the Burrows-Wheeler algorithm. It only supports compressing single files and its compression is slow, however is more effective than Deflate (.gz & .zip).";
|
||||
this.infoURL = "https://wikipedia.org/wiki/Bzip2";
|
||||
this.inputType = "ArrayBuffer";
|
||||
this.outputType = "ArrayBuffer";
|
||||
this.args = [
|
||||
{
|
||||
name: "Block size (100s of kb)",
|
||||
type: "number",
|
||||
value: 9,
|
||||
min: 1,
|
||||
max: 9
|
||||
},
|
||||
{
|
||||
name: "Work factor",
|
||||
type: "number",
|
||||
value: 30
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ArrayBuffer} input
|
||||
* @param {Object[]} args
|
||||
* @returns {File}
|
||||
*/
|
||||
run(input, args) {
|
||||
const [blockSize, workFactor] = args;
|
||||
if (input.byteLength <= 0) {
|
||||
throw new OperationError("Please provide an input.");
|
||||
}
|
||||
if (ENVIRONMENT_IS_WORKER()) self.sendStatusMessage("Loading Bzip2...");
|
||||
return new Promise((resolve, reject) => {
|
||||
Bzip2().then(bzip2 => {
|
||||
if (ENVIRONMENT_IS_WORKER()) self.sendStatusMessage("Compressing data...");
|
||||
const inpArray = new Uint8Array(input);
|
||||
const bzip2cc = bzip2.compressBZ2(inpArray, blockSize, workFactor);
|
||||
if (bzip2cc.error !== 0) {
|
||||
reject(new OperationError(bzip2cc.error_msg));
|
||||
} else {
|
||||
const output = bzip2cc.output;
|
||||
resolve(output.buffer.slice(output.byteOffset, output.byteLength + output.byteOffset));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default Bzip2Compress;
|
||||
@@ -1,12 +1,12 @@
|
||||
/**
|
||||
* @author n1474335 [n1474335@gmail.com]
|
||||
* @copyright Crown Copyright 2016
|
||||
* @author Matt C [me@mitt.dev]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import bzip2 from "../vendor/bzip2";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import Bzip2 from "libbzip2-wasm";
|
||||
|
||||
/**
|
||||
* Bzip2 Decompress operation
|
||||
@@ -23,9 +23,15 @@ class Bzip2Decompress extends Operation {
|
||||
this.module = "Compression";
|
||||
this.description = "Decompresses data using the Bzip2 algorithm.";
|
||||
this.infoURL = "https://wikipedia.org/wiki/Bzip2";
|
||||
this.inputType = "byteArray";
|
||||
this.outputType = "string";
|
||||
this.args = [];
|
||||
this.inputType = "ArrayBuffer";
|
||||
this.outputType = "ArrayBuffer";
|
||||
this.args = [
|
||||
{
|
||||
name: "Use low-memory, slower decompression algorithm",
|
||||
type: "boolean",
|
||||
value: false
|
||||
}
|
||||
];
|
||||
this.patterns = [
|
||||
{
|
||||
"match": "^\\x42\\x5a\\x68",
|
||||
@@ -41,14 +47,24 @@ class Bzip2Decompress extends Operation {
|
||||
* @returns {string}
|
||||
*/
|
||||
run(input, args) {
|
||||
const compressed = new Uint8Array(input);
|
||||
|
||||
try {
|
||||
const bzip2Reader = bzip2.array(compressed);
|
||||
return bzip2.simple(bzip2Reader);
|
||||
} catch (err) {
|
||||
throw new OperationError(err);
|
||||
const [small] = args;
|
||||
if (input.byteLength <= 0) {
|
||||
throw new OperationError("Please provide an input.");
|
||||
}
|
||||
if (ENVIRONMENT_IS_WORKER()) self.sendStatusMessage("Loading Bzip2...");
|
||||
return new Promise((resolve, reject) => {
|
||||
Bzip2().then(bzip2 => {
|
||||
if (ENVIRONMENT_IS_WORKER()) self.sendStatusMessage("Decompressing data...");
|
||||
const inpArray = new Uint8Array(input);
|
||||
const bzip2cc = bzip2.decompressBZ2(inpArray, small ? 1 : 0);
|
||||
if (bzip2cc.error !== 0) {
|
||||
reject(new OperationError(bzip2cc.error_msg));
|
||||
} else {
|
||||
const output = bzip2cc.output;
|
||||
resolve(output.buffer.slice(output.byteOffset, output.byteLength + output.byteOffset));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
143
src/core/operations/ContainImage.mjs
Normal file
143
src/core/operations/ContainImage.mjs
Normal file
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* @author j433866 [j433866@gmail.com]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import { isImage } from "../lib/FileType";
|
||||
import { toBase64 } from "../lib/Base64.mjs";
|
||||
import jimp from "jimp";
|
||||
|
||||
/**
|
||||
* Contain Image operation
|
||||
*/
|
||||
class ContainImage extends Operation {
|
||||
|
||||
/**
|
||||
* ContainImage constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Contain Image";
|
||||
this.module = "Image";
|
||||
this.description = "Scales an image to the specified width and height, maintaining the aspect ratio. The image may be letterboxed.";
|
||||
this.infoURL = "";
|
||||
this.inputType = "byteArray";
|
||||
this.outputType = "byteArray";
|
||||
this.presentType = "html";
|
||||
this.args = [
|
||||
{
|
||||
name: "Width",
|
||||
type: "number",
|
||||
value: 100,
|
||||
min: 1
|
||||
},
|
||||
{
|
||||
name: "Height",
|
||||
type: "number",
|
||||
value: 100,
|
||||
min: 1
|
||||
},
|
||||
{
|
||||
name: "Horizontal align",
|
||||
type: "option",
|
||||
value: [
|
||||
"Left",
|
||||
"Center",
|
||||
"Right"
|
||||
],
|
||||
defaultIndex: 1
|
||||
},
|
||||
{
|
||||
name: "Vertical align",
|
||||
type: "option",
|
||||
value: [
|
||||
"Top",
|
||||
"Middle",
|
||||
"Bottom"
|
||||
],
|
||||
defaultIndex: 1
|
||||
},
|
||||
{
|
||||
name: "Resizing algorithm",
|
||||
type: "option",
|
||||
value: [
|
||||
"Nearest Neighbour",
|
||||
"Bilinear",
|
||||
"Bicubic",
|
||||
"Hermite",
|
||||
"Bezier"
|
||||
],
|
||||
defaultIndex: 1
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {byteArray} input
|
||||
* @param {Object[]} args
|
||||
* @returns {byteArray}
|
||||
*/
|
||||
async run(input, args) {
|
||||
const [width, height, hAlign, vAlign, alg] = args;
|
||||
|
||||
const resizeMap = {
|
||||
"Nearest Neighbour": jimp.RESIZE_NEAREST_NEIGHBOR,
|
||||
"Bilinear": jimp.RESIZE_BILINEAR,
|
||||
"Bicubic": jimp.RESIZE_BICUBIC,
|
||||
"Hermite": jimp.RESIZE_HERMITE,
|
||||
"Bezier": jimp.RESIZE_BEZIER
|
||||
};
|
||||
|
||||
const alignMap = {
|
||||
"Left": jimp.HORIZONTAL_ALIGN_LEFT,
|
||||
"Center": jimp.HORIZONTAL_ALIGN_CENTER,
|
||||
"Right": jimp.HORIZONTAL_ALIGN_RIGHT,
|
||||
"Top": jimp.VERTICAL_ALIGN_TOP,
|
||||
"Middle": jimp.VERTICAL_ALIGN_MIDDLE,
|
||||
"Bottom": jimp.VERTICAL_ALIGN_BOTTOM
|
||||
};
|
||||
|
||||
if (!isImage(input)) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
let image;
|
||||
try {
|
||||
image = await jimp.read(Buffer.from(input));
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error loading image. (${err})`);
|
||||
}
|
||||
try {
|
||||
if (ENVIRONMENT_IS_WORKER())
|
||||
self.sendStatusMessage("Containing image...");
|
||||
image.contain(width, height, alignMap[hAlign] | alignMap[vAlign], resizeMap[alg]);
|
||||
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
|
||||
return [...imageBuffer];
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error containing image. (${err})`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the contained image using HTML for web apps
|
||||
* @param {byteArray} data
|
||||
* @returns {html}
|
||||
*/
|
||||
present(data) {
|
||||
if (!data.length) return "";
|
||||
|
||||
const type = isImage(data);
|
||||
if (!type) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
return `<img src="data:${type};base64,${toBase64(data)}">`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default ContainImage;
|
||||
143
src/core/operations/CoverImage.mjs
Normal file
143
src/core/operations/CoverImage.mjs
Normal file
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* @author j433866 [j433866@gmail.com]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import { isImage } from "../lib/FileType";
|
||||
import { toBase64 } from "../lib/Base64.mjs";
|
||||
import jimp from "jimp";
|
||||
|
||||
/**
|
||||
* Cover Image operation
|
||||
*/
|
||||
class CoverImage extends Operation {
|
||||
|
||||
/**
|
||||
* CoverImage constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Cover Image";
|
||||
this.module = "Image";
|
||||
this.description = "Scales the image to the given width and height, keeping the aspect ratio. The image may be clipped.";
|
||||
this.infoURL = "";
|
||||
this.inputType = "byteArray";
|
||||
this.outputType = "byteArray";
|
||||
this.presentType = "html";
|
||||
this.args = [
|
||||
{
|
||||
name: "Width",
|
||||
type: "number",
|
||||
value: 100,
|
||||
min: 1
|
||||
},
|
||||
{
|
||||
name: "Height",
|
||||
type: "number",
|
||||
value: 100,
|
||||
min: 1
|
||||
},
|
||||
{
|
||||
name: "Horizontal align",
|
||||
type: "option",
|
||||
value: [
|
||||
"Left",
|
||||
"Center",
|
||||
"Right"
|
||||
],
|
||||
defaultIndex: 1
|
||||
},
|
||||
{
|
||||
name: "Vertical align",
|
||||
type: "option",
|
||||
value: [
|
||||
"Top",
|
||||
"Middle",
|
||||
"Bottom"
|
||||
],
|
||||
defaultIndex: 1
|
||||
},
|
||||
{
|
||||
name: "Resizing algorithm",
|
||||
type: "option",
|
||||
value: [
|
||||
"Nearest Neighbour",
|
||||
"Bilinear",
|
||||
"Bicubic",
|
||||
"Hermite",
|
||||
"Bezier"
|
||||
],
|
||||
defaultIndex: 1
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {byteArray} input
|
||||
* @param {Object[]} args
|
||||
* @returns {byteArray}
|
||||
*/
|
||||
async run(input, args) {
|
||||
const [width, height, hAlign, vAlign, alg] = args;
|
||||
|
||||
const resizeMap = {
|
||||
"Nearest Neighbour": jimp.RESIZE_NEAREST_NEIGHBOR,
|
||||
"Bilinear": jimp.RESIZE_BILINEAR,
|
||||
"Bicubic": jimp.RESIZE_BICUBIC,
|
||||
"Hermite": jimp.RESIZE_HERMITE,
|
||||
"Bezier": jimp.RESIZE_BEZIER
|
||||
};
|
||||
|
||||
const alignMap = {
|
||||
"Left": jimp.HORIZONTAL_ALIGN_LEFT,
|
||||
"Center": jimp.HORIZONTAL_ALIGN_CENTER,
|
||||
"Right": jimp.HORIZONTAL_ALIGN_RIGHT,
|
||||
"Top": jimp.VERTICAL_ALIGN_TOP,
|
||||
"Middle": jimp.VERTICAL_ALIGN_MIDDLE,
|
||||
"Bottom": jimp.VERTICAL_ALIGN_BOTTOM
|
||||
};
|
||||
|
||||
if (!isImage(input)) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
let image;
|
||||
try {
|
||||
image = await jimp.read(Buffer.from(input));
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error loading image. (${err})`);
|
||||
}
|
||||
try {
|
||||
if (ENVIRONMENT_IS_WORKER())
|
||||
self.sendStatusMessage("Covering image...");
|
||||
image.cover(width, height, alignMap[hAlign] | alignMap[vAlign], resizeMap[alg]);
|
||||
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
|
||||
return [...imageBuffer];
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error covering image. (${err})`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the covered image using HTML for web apps
|
||||
* @param {byteArray} data
|
||||
* @returns {html}
|
||||
*/
|
||||
present(data) {
|
||||
if (!data.length) return "";
|
||||
|
||||
const type = isImage(data);
|
||||
if (!type) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
return `<img src="data:${type};base64,${toBase64(data)}">`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default CoverImage;
|
||||
144
src/core/operations/CropImage.mjs
Normal file
144
src/core/operations/CropImage.mjs
Normal file
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* @author j433866 [j433866@gmail.com]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import { isImage } from "../lib/FileType";
|
||||
import { toBase64 } from "../lib/Base64.mjs";
|
||||
import jimp from "jimp";
|
||||
|
||||
/**
|
||||
* Crop Image operation
|
||||
*/
|
||||
class CropImage extends Operation {
|
||||
|
||||
/**
|
||||
* CropImage constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Crop Image";
|
||||
this.module = "Image";
|
||||
this.description = "Crops an image to the specified region, or automatically crops edges.<br><br><b><u>Autocrop</u></b><br>Automatically crops same-colour borders from the image.<br><br><u>Autocrop tolerance</u><br>A percentage value for the tolerance of colour difference between pixels.<br><br><u>Only autocrop frames</u><br>Only crop real frames (all sides must have the same border)<br><br><u>Symmetric autocrop</u><br>Force autocrop to be symmetric (top/bottom and left/right are cropped by the same amount)<br><br><u>Autocrop keep border</u><br>The number of pixels of border to leave around the image.";
|
||||
this.infoURL = "https://wikipedia.org/wiki/Cropping_(image)";
|
||||
this.inputType = "byteArray";
|
||||
this.outputType = "byteArray";
|
||||
this.presentType = "html";
|
||||
this.args = [
|
||||
{
|
||||
name: "X Position",
|
||||
type: "number",
|
||||
value: 0,
|
||||
min: 0
|
||||
},
|
||||
{
|
||||
name: "Y Position",
|
||||
type: "number",
|
||||
value: 0,
|
||||
min: 0
|
||||
},
|
||||
{
|
||||
name: "Width",
|
||||
type: "number",
|
||||
value: 10,
|
||||
min: 1
|
||||
},
|
||||
{
|
||||
name: "Height",
|
||||
type: "number",
|
||||
value: 10,
|
||||
min: 1
|
||||
},
|
||||
{
|
||||
name: "Autocrop",
|
||||
type: "boolean",
|
||||
value: false
|
||||
},
|
||||
{
|
||||
name: "Autocrop tolerance (%)",
|
||||
type: "number",
|
||||
value: 0.02,
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 0.01
|
||||
},
|
||||
{
|
||||
name: "Only autocrop frames",
|
||||
type: "boolean",
|
||||
value: true
|
||||
},
|
||||
{
|
||||
name: "Symmetric autocrop",
|
||||
type: "boolean",
|
||||
value: false
|
||||
},
|
||||
{
|
||||
name: "Autocrop keep border (px)",
|
||||
type: "number",
|
||||
value: 0,
|
||||
min: 0
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {byteArray} input
|
||||
* @param {Object[]} args
|
||||
* @returns {byteArray}
|
||||
*/
|
||||
async run(input, args) {
|
||||
const [xPos, yPos, width, height, autocrop, autoTolerance, autoFrames, autoSymmetric, autoBorder] = args;
|
||||
if (!isImage(input)) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
let image;
|
||||
try {
|
||||
image = await jimp.read(Buffer.from(input));
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error loading image. (${err})`);
|
||||
}
|
||||
try {
|
||||
if (ENVIRONMENT_IS_WORKER())
|
||||
self.sendStatusMessage("Cropping image...");
|
||||
if (autocrop) {
|
||||
image.autocrop({
|
||||
tolerance: (autoTolerance / 100),
|
||||
cropOnlyFrames: autoFrames,
|
||||
cropSymmetric: autoSymmetric,
|
||||
leaveBorder: autoBorder
|
||||
});
|
||||
} else {
|
||||
image.crop(xPos, yPos, width, height);
|
||||
}
|
||||
|
||||
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
|
||||
return [...imageBuffer];
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error cropping image. (${err})`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the cropped image using HTML for web apps
|
||||
* @param {byteArray} data
|
||||
* @returns {html}
|
||||
*/
|
||||
present(data) {
|
||||
if (!data.length) return "";
|
||||
|
||||
const type = isImage(data);
|
||||
if (!type) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
return `<img src="data:${type};base64,${toBase64(data)}">`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default CropImage;
|
||||
125
src/core/operations/DNSOverHTTPS.mjs
Normal file
125
src/core/operations/DNSOverHTTPS.mjs
Normal file
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* @author h345983745
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
|
||||
/**
|
||||
* DNS over HTTPS operation
|
||||
*/
|
||||
class DNSOverHTTPS extends Operation {
|
||||
|
||||
/**
|
||||
* DNSOverHTTPS constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "DNS over HTTPS";
|
||||
this.module = "Default";
|
||||
this.description = [
|
||||
"Takes a single domain name and performs a DNS lookup using DNS over HTTPS.",
|
||||
"<br><br>",
|
||||
"By default, <a href='https://developers.cloudflare.com/1.1.1.1/dns-over-https/'>Cloudflare</a> and <a href='https://developers.google.com/speed/public-dns/docs/dns-over-https'>Google</a> DNS over HTTPS services are supported.",
|
||||
"<br><br>",
|
||||
"Can be used with any service that supports the GET parameters <code>name</code> and <code>type</code>."
|
||||
].join("\n");
|
||||
this.infoURL = "https://wikipedia.org/wiki/DNS_over_HTTPS";
|
||||
this.inputType = "string";
|
||||
this.outputType = "JSON";
|
||||
this.manualBake = true;
|
||||
this.args = [
|
||||
{
|
||||
name: "Resolver",
|
||||
type: "editableOption",
|
||||
value: [
|
||||
{
|
||||
name: "Google",
|
||||
value: "https://dns.google.com/resolve"
|
||||
},
|
||||
{
|
||||
name: "Cloudflare",
|
||||
value: "https://cloudflare-dns.com/dns-query"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Request Type",
|
||||
type: "option",
|
||||
value: [
|
||||
"A",
|
||||
"AAAA",
|
||||
"TXT",
|
||||
"MX",
|
||||
"DNSKEY",
|
||||
"NS"
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Answer Data Only",
|
||||
type: "boolean",
|
||||
value: false
|
||||
},
|
||||
{
|
||||
name: "Validate DNSSEC",
|
||||
type: "boolean",
|
||||
value: true
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} input
|
||||
* @param {Object[]} args
|
||||
* @returns {JSON}
|
||||
*/
|
||||
run(input, args) {
|
||||
const [resolver, requestType, justAnswer, DNSSEC] = args;
|
||||
let url = URL;
|
||||
try {
|
||||
url = new URL(resolver);
|
||||
} catch (error) {
|
||||
throw new OperationError(error.toString() +
|
||||
"\n\nThis error could be caused by one of the following:\n" +
|
||||
" - An invalid Resolver URL\n");
|
||||
}
|
||||
const params = {name: input, type: requestType, cd: DNSSEC};
|
||||
|
||||
url.search = new URLSearchParams(params);
|
||||
|
||||
return fetch(url, {headers: {"accept": "application/dns-json"}}).then(response => {
|
||||
return response.json();
|
||||
}).then(data => {
|
||||
if (justAnswer) {
|
||||
return extractData(data.Answer);
|
||||
}
|
||||
return data;
|
||||
}).catch(e => {
|
||||
throw new OperationError(`Error making request to ${url}\n${e.toString()}`);
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct an array of just data from a DNS Answer section
|
||||
*
|
||||
* @private
|
||||
* @param {JSON} data
|
||||
* @returns {JSON}
|
||||
*/
|
||||
function extractData(data) {
|
||||
if (typeof(data) == "undefined"){
|
||||
return [];
|
||||
} else {
|
||||
const dataValues = [];
|
||||
data.forEach(element => {
|
||||
dataValues.push(element.data);
|
||||
});
|
||||
return dataValues;
|
||||
}
|
||||
}
|
||||
|
||||
export default DNSOverHTTPS;
|
||||
@@ -5,7 +5,8 @@
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import Magic from "../lib/Magic";
|
||||
import {detectFileType} from "../lib/FileType";
|
||||
import {FILE_SIGNATURES} from "../lib/FileSignatures";
|
||||
|
||||
/**
|
||||
* Detect File Type operation
|
||||
@@ -24,7 +25,13 @@ class DetectFileType extends Operation {
|
||||
this.infoURL = "https://wikipedia.org/wiki/List_of_file_signatures";
|
||||
this.inputType = "ArrayBuffer";
|
||||
this.outputType = "string";
|
||||
this.args = [];
|
||||
this.args = Object.keys(FILE_SIGNATURES).map(cat => {
|
||||
return {
|
||||
name: cat,
|
||||
type: "boolean",
|
||||
value: true
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -34,17 +41,27 @@ class DetectFileType extends Operation {
|
||||
*/
|
||||
run(input, args) {
|
||||
const data = new Uint8Array(input),
|
||||
type = Magic.magicFileType(data);
|
||||
categories = [];
|
||||
|
||||
if (!type) {
|
||||
args.forEach((cat, i) => {
|
||||
if (cat) categories.push(Object.keys(FILE_SIGNATURES)[i]);
|
||||
});
|
||||
|
||||
const types = detectFileType(data, categories);
|
||||
|
||||
if (!types.length) {
|
||||
return "Unknown file type. Have you tried checking the entropy of this data to determine whether it might be encrypted or compressed?";
|
||||
} else {
|
||||
let output = "File extension: " + type.ext + "\n" +
|
||||
"MIME type: " + type.mime;
|
||||
let output = "";
|
||||
|
||||
if (type.desc && type.desc.length) {
|
||||
output += "\nDescription: " + type.desc;
|
||||
}
|
||||
types.forEach(type => {
|
||||
output += "File extension: " + type.extension + "\n" +
|
||||
"MIME type: " + type.mime + "\n";
|
||||
|
||||
if (type.description && type.description.length) {
|
||||
output += "\nDescription: " + type.description + "\n";
|
||||
}
|
||||
});
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
79
src/core/operations/DitherImage.mjs
Normal file
79
src/core/operations/DitherImage.mjs
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* @author j433866 [j433866@gmail.com]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import { isImage } from "../lib/FileType";
|
||||
import { toBase64 } from "../lib/Base64";
|
||||
import jimp from "jimp";
|
||||
|
||||
/**
|
||||
* Image Dither operation
|
||||
*/
|
||||
class DitherImage extends Operation {
|
||||
|
||||
/**
|
||||
* DitherImage constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Dither Image";
|
||||
this.module = "Image";
|
||||
this.description = "Apply a dither effect to an image.";
|
||||
this.infoURL = "https://wikipedia.org/wiki/Dither";
|
||||
this.inputType = "byteArray";
|
||||
this.outputType = "byteArray";
|
||||
this.presentType = "html";
|
||||
this.args = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {byteArray} input
|
||||
* @param {Object[]} args
|
||||
* @returns {byteArray}
|
||||
*/
|
||||
async run(input, args) {
|
||||
if (!isImage(input)) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
let image;
|
||||
try {
|
||||
image = await jimp.read(Buffer.from(input));
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error loading image. (${err})`);
|
||||
}
|
||||
try {
|
||||
if (ENVIRONMENT_IS_WORKER())
|
||||
self.sendStatusMessage("Applying dither to image...");
|
||||
image.dither565();
|
||||
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
|
||||
return [...imageBuffer];
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error applying dither to image. (${err})`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the dithered image using HTML for web apps
|
||||
* @param {byteArray} data
|
||||
* @returns {html}
|
||||
*/
|
||||
present(data) {
|
||||
if (!data.length) return "";
|
||||
|
||||
const type = isImage(data);
|
||||
if (!type) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
return `<img src="data:${type};base64,${toBase64(data)}">`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default DitherImage;
|
||||
214
src/core/operations/Enigma.mjs
Normal file
214
src/core/operations/Enigma.mjs
Normal file
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* Emulation of the Enigma machine.
|
||||
*
|
||||
* @author s2224834
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import {ROTORS, LETTERS, ROTORS_FOURTH, REFLECTORS, Rotor, Reflector, Plugboard, EnigmaMachine} from "../lib/Enigma";
|
||||
|
||||
/**
|
||||
* Enigma operation
|
||||
*/
|
||||
class Enigma extends Operation {
|
||||
/**
|
||||
* Enigma constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Enigma";
|
||||
this.module = "Default";
|
||||
this.description = "Encipher/decipher with the WW2 Enigma machine.<br><br>Enigma was used by the German military, among others, around the WW2 era as a portable cipher machine to protect sensitive military, diplomatic and commercial communications.<br><br>The standard set of German military rotors and reflectors are provided. To configure the plugboard, enter a string of connected pairs of letters, e.g. <code>AB CD EF</code> connects A to B, C to D, and E to F. This is also used to create your own reflectors. To create your own rotor, enter the letters that the rotor maps A to Z to, in order, optionally followed by <code><</code> then a list of stepping points.<br>This is deliberately fairly permissive with rotor placements etc compared to a real Enigma (on which, for example, a four-rotor Enigma uses only the thin reflectors and the beta or gamma rotor in the 4th slot).<br><br>More detailed descriptions of the Enigma, Typex and Bombe operations <a href='https://github.com/gchq/CyberChef/wiki/Enigma,-the-Bombe,-and-Typex'>can be found here</a>.";
|
||||
this.infoURL = "https://wikipedia.org/wiki/Enigma_machine";
|
||||
this.inputType = "string";
|
||||
this.outputType = "string";
|
||||
this.args = [
|
||||
{
|
||||
name: "Model",
|
||||
type: "argSelector",
|
||||
value: [
|
||||
{
|
||||
name: "3-rotor",
|
||||
off: [1, 2, 3]
|
||||
},
|
||||
{
|
||||
name: "4-rotor",
|
||||
on: [1, 2, 3]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "Left-most (4th) rotor",
|
||||
type: "editableOption",
|
||||
value: ROTORS_FOURTH,
|
||||
defaultIndex: 0
|
||||
},
|
||||
{
|
||||
name: "Left-most rotor ring setting",
|
||||
type: "option",
|
||||
value: LETTERS
|
||||
},
|
||||
{
|
||||
name: "Left-most rotor initial value",
|
||||
type: "option",
|
||||
value: LETTERS
|
||||
},
|
||||
{
|
||||
name: "Left-hand rotor",
|
||||
type: "editableOption",
|
||||
value: ROTORS,
|
||||
defaultIndex: 0
|
||||
},
|
||||
{
|
||||
name: "Left-hand rotor ring setting",
|
||||
type: "option",
|
||||
value: LETTERS
|
||||
},
|
||||
{
|
||||
name: "Left-hand rotor initial value",
|
||||
type: "option",
|
||||
value: LETTERS
|
||||
},
|
||||
{
|
||||
name: "Middle rotor",
|
||||
type: "editableOption",
|
||||
value: ROTORS,
|
||||
defaultIndex: 1
|
||||
},
|
||||
{
|
||||
name: "Middle rotor ring setting",
|
||||
type: "option",
|
||||
value: LETTERS
|
||||
},
|
||||
{
|
||||
name: "Middle rotor initial value",
|
||||
type: "option",
|
||||
value: LETTERS
|
||||
},
|
||||
{
|
||||
name: "Right-hand rotor",
|
||||
type: "editableOption",
|
||||
value: ROTORS,
|
||||
// Default config is the rotors I-III *left to right*
|
||||
defaultIndex: 2
|
||||
},
|
||||
{
|
||||
name: "Right-hand rotor ring setting",
|
||||
type: "option",
|
||||
value: LETTERS
|
||||
},
|
||||
{
|
||||
name: "Right-hand rotor initial value",
|
||||
type: "option",
|
||||
value: LETTERS
|
||||
},
|
||||
{
|
||||
name: "Reflector",
|
||||
type: "editableOption",
|
||||
value: REFLECTORS
|
||||
},
|
||||
{
|
||||
name: "Plugboard",
|
||||
type: "string",
|
||||
value: ""
|
||||
},
|
||||
{
|
||||
name: "Strict output",
|
||||
hint: "Remove non-alphabet letters and group output",
|
||||
type: "boolean",
|
||||
value: true
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper - for ease of use rotors are specified as a single string; this
|
||||
* method breaks the spec string into wiring and steps parts.
|
||||
*
|
||||
* @param {string} rotor - Rotor specification string.
|
||||
* @param {number} i - For error messages, the number of this rotor.
|
||||
* @returns {string[]}
|
||||
*/
|
||||
parseRotorStr(rotor, i) {
|
||||
if (rotor === "") {
|
||||
throw new OperationError(`Rotor ${i} must be provided.`);
|
||||
}
|
||||
if (!rotor.includes("<")) {
|
||||
return [rotor, ""];
|
||||
}
|
||||
return rotor.split("<", 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} input
|
||||
* @param {Object[]} args
|
||||
* @returns {string}
|
||||
*/
|
||||
run(input, args) {
|
||||
const model = args[0];
|
||||
const reflectorstr = args[13];
|
||||
const plugboardstr = args[14];
|
||||
const removeOther = args[15];
|
||||
const rotors = [];
|
||||
for (let i=0; i<4; i++) {
|
||||
if (i === 0 && model === "3-rotor") {
|
||||
// Skip the 4th rotor settings
|
||||
continue;
|
||||
}
|
||||
const [rotorwiring, rotorsteps] = this.parseRotorStr(args[i*3 + 1], 1);
|
||||
rotors.push(new Rotor(rotorwiring, rotorsteps, args[i*3 + 2], args[i*3 + 3]));
|
||||
}
|
||||
// Rotors are handled in reverse
|
||||
rotors.reverse();
|
||||
const reflector = new Reflector(reflectorstr);
|
||||
const plugboard = new Plugboard(plugboardstr);
|
||||
if (removeOther) {
|
||||
input = input.replace(/[^A-Za-z]/g, "");
|
||||
}
|
||||
const enigma = new EnigmaMachine(rotors, reflector, plugboard);
|
||||
let result = enigma.crypt(input);
|
||||
if (removeOther) {
|
||||
// Five character cipher groups is traditional
|
||||
result = result.replace(/([A-Z]{5})(?!$)/g, "$1 ");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlight Enigma
|
||||
* This is only possible if we're passing through non-alphabet characters.
|
||||
*
|
||||
* @param {Object[]} pos
|
||||
* @param {number} pos[].start
|
||||
* @param {number} pos[].end
|
||||
* @param {Object[]} args
|
||||
* @returns {Object[]} pos
|
||||
*/
|
||||
highlight(pos, args) {
|
||||
if (args[13] === false) {
|
||||
return pos;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlight Enigma in reverse
|
||||
*
|
||||
* @param {Object[]} pos
|
||||
* @param {number} pos[].start
|
||||
* @param {number} pos[].end
|
||||
* @param {Object[]} args
|
||||
* @returns {Object[]} pos
|
||||
*/
|
||||
highlightReverse(pos, args) {
|
||||
if (args[13] === false) {
|
||||
return pos;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default Enigma;
|
||||
@@ -4,8 +4,13 @@
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import * as d3temp from "d3";
|
||||
import * as nodomtemp from "nodom";
|
||||
|
||||
import Operation from "../Operation";
|
||||
import Utils from "../Utils";
|
||||
|
||||
const d3 = d3temp.default ? d3temp.default : d3temp;
|
||||
const nodom = nodomtemp.default ? nodomtemp.default: nodomtemp;
|
||||
|
||||
/**
|
||||
* Entropy operation
|
||||
@@ -19,30 +24,45 @@ class Entropy extends Operation {
|
||||
super();
|
||||
|
||||
this.name = "Entropy";
|
||||
this.module = "Default";
|
||||
this.module = "Charts";
|
||||
this.description = "Shannon Entropy, in the context of information theory, is a measure of the rate at which information is produced by a source of data. It can be used, in a broad sense, to detect whether data is likely to be structured or unstructured. 8 is the maximum, representing highly unstructured, 'random' data. English language text usually falls somewhere between 3.5 and 5. Properly encrypted or compressed data should have an entropy of over 7.5.";
|
||||
this.infoURL = "https://wikipedia.org/wiki/Entropy_(information_theory)";
|
||||
this.inputType = "byteArray";
|
||||
this.outputType = "number";
|
||||
this.inputType = "ArrayBuffer";
|
||||
this.outputType = "json";
|
||||
this.presentType = "html";
|
||||
this.args = [];
|
||||
this.args = [
|
||||
{
|
||||
"name": "Visualisation",
|
||||
"type": "option",
|
||||
"value": ["Shannon scale", "Histogram (Bar)", "Histogram (Line)", "Curve", "Image"]
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {byteArray} input
|
||||
* @param {Object[]} args
|
||||
* Calculates the frequency of bytes in the input.
|
||||
*
|
||||
* @param {Uint8Array} input
|
||||
* @returns {number}
|
||||
*/
|
||||
run(input, args) {
|
||||
calculateShannonEntropy(input) {
|
||||
const prob = [],
|
||||
uniques = input.unique(),
|
||||
str = Utils.byteArrayToChars(input);
|
||||
let i;
|
||||
occurrences = new Array(256).fill(0);
|
||||
|
||||
for (i = 0; i < uniques.length; i++) {
|
||||
prob.push(str.count(Utils.chr(uniques[i])) / input.length);
|
||||
// Count occurrences of each byte in the input
|
||||
let i;
|
||||
for (i = 0; i < input.length; i++) {
|
||||
occurrences[input[i]]++;
|
||||
}
|
||||
|
||||
// Store probability list
|
||||
for (i = 0; i < occurrences.length; i++) {
|
||||
if (occurrences[i] > 0) {
|
||||
prob.push(occurrences[i] / input.length);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate Shannon entropy
|
||||
let entropy = 0,
|
||||
p;
|
||||
|
||||
@@ -54,44 +74,357 @@ class Entropy extends Operation {
|
||||
return -entropy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the scanning entropy of the input
|
||||
*
|
||||
* @param {Uint8Array} inputBytes
|
||||
* @returns {Object}
|
||||
*/
|
||||
calculateScanningEntropy(inputBytes) {
|
||||
const entropyData = [];
|
||||
const binWidth = inputBytes.length < 256 ? 8 : 256;
|
||||
|
||||
for (let bytePos = 0; bytePos < inputBytes.length; bytePos += binWidth) {
|
||||
const block = inputBytes.slice(bytePos, bytePos+binWidth);
|
||||
entropyData.push(this.calculateShannonEntropy(block));
|
||||
}
|
||||
|
||||
return { entropyData, binWidth };
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the frequency of bytes in the input.
|
||||
*
|
||||
* @param {object} svg
|
||||
* @param {function} xScale
|
||||
* @param {function} yScale
|
||||
* @param {integer} svgHeight
|
||||
* @param {integer} svgWidth
|
||||
* @param {object} margins
|
||||
* @param {string} xTitle
|
||||
* @param {string} yTitle
|
||||
*/
|
||||
createAxes(svg, xScale, yScale, svgHeight, svgWidth, margins, title, xTitle, yTitle) {
|
||||
// Axes
|
||||
const yAxis = d3.axisLeft()
|
||||
.scale(yScale);
|
||||
|
||||
const xAxis = d3.axisBottom()
|
||||
.scale(xScale);
|
||||
|
||||
svg.append("g")
|
||||
.attr("transform", `translate(0, ${svgHeight - margins.bottom})`)
|
||||
.call(xAxis);
|
||||
|
||||
svg.append("g")
|
||||
.attr("transform", `translate(${margins.left},0)`)
|
||||
.call(yAxis);
|
||||
|
||||
// Axes labels
|
||||
svg.append("text")
|
||||
.attr("transform", "rotate(-90)")
|
||||
.attr("y", 0 - margins.left)
|
||||
.attr("x", 0 - (svgHeight / 2))
|
||||
.attr("dy", "1em")
|
||||
.style("text-anchor", "middle")
|
||||
.text(yTitle);
|
||||
|
||||
svg.append("text")
|
||||
.attr("transform", `translate(${svgWidth / 2}, ${svgHeight - margins.bottom + 40})`)
|
||||
.style("text-anchor", "middle")
|
||||
.text(xTitle);
|
||||
|
||||
// Add title
|
||||
svg.append("text")
|
||||
.attr("transform", `translate(${svgWidth / 2}, ${margins.top - 10})`)
|
||||
.style("text-anchor", "middle")
|
||||
.text(title);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the frequency of bytes in the input.
|
||||
*
|
||||
* @param {Uint8Array} inputBytes
|
||||
* @returns {number[]}
|
||||
*/
|
||||
calculateByteFrequency(inputBytes) {
|
||||
const freq = new Array(256).fill(0);
|
||||
if (inputBytes.length === 0) return freq;
|
||||
|
||||
// Count occurrences of each byte in the input
|
||||
let i;
|
||||
for (i = 0; i < inputBytes.length; i++) {
|
||||
freq[inputBytes[i]]++;
|
||||
}
|
||||
|
||||
for (i = 0; i < freq.length; i++) {
|
||||
freq[i] = freq[i] / inputBytes.length;
|
||||
}
|
||||
|
||||
return freq;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the frequency of bytes in the input.
|
||||
*
|
||||
* @param {number[]} byteFrequency
|
||||
* @returns {HTML}
|
||||
*/
|
||||
createByteFrequencyLineHistogram(byteFrequency) {
|
||||
const margins = { top: 30, right: 20, bottom: 50, left: 30 };
|
||||
|
||||
const svgWidth = 500,
|
||||
svgHeight = 500;
|
||||
|
||||
const document = new nodom.Document();
|
||||
let svg = document.createElement("svg");
|
||||
|
||||
svg = d3.select(svg)
|
||||
.attr("width", "100%")
|
||||
.attr("height", "100%")
|
||||
.attr("viewBox", `0 0 ${svgWidth} ${svgHeight}`);
|
||||
|
||||
const yScale = d3.scaleLinear()
|
||||
.domain([0, d3.max(byteFrequency, d => d)])
|
||||
.range([svgHeight - margins.bottom, margins.top]);
|
||||
|
||||
const xScale = d3.scaleLinear()
|
||||
.domain([0, byteFrequency.length - 1])
|
||||
.range([margins.left, svgWidth - margins.right]);
|
||||
|
||||
const line = d3.line()
|
||||
.x((_, i) => xScale(i))
|
||||
.y(d => yScale(d))
|
||||
.curve(d3.curveMonotoneX);
|
||||
|
||||
svg.append("path")
|
||||
.datum(byteFrequency)
|
||||
.attr("fill", "none")
|
||||
.attr("stroke", "steelblue")
|
||||
.attr("d", line);
|
||||
|
||||
this.createAxes(svg, xScale, yScale, svgHeight, svgWidth, margins, "", "Byte", "Byte Frequency");
|
||||
|
||||
return svg._groups[0][0].outerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a byte frequency histogram
|
||||
*
|
||||
* @param {number[]} byteFrequency
|
||||
* @returns {HTML}
|
||||
*/
|
||||
createByteFrequencyBarHistogram(byteFrequency) {
|
||||
const margins = { top: 30, right: 20, bottom: 50, left: 30 };
|
||||
|
||||
const svgWidth = 500,
|
||||
svgHeight = 500,
|
||||
binWidth = 1;
|
||||
|
||||
const document = new nodom.Document();
|
||||
let svg = document.createElement("svg");
|
||||
svg = d3.select(svg)
|
||||
.attr("width", "100%")
|
||||
.attr("height", "100%")
|
||||
.attr("viewBox", `0 0 ${svgWidth} ${svgHeight}`);
|
||||
|
||||
const yExtent = d3.extent(byteFrequency, d => d);
|
||||
const yScale = d3.scaleLinear()
|
||||
.domain(yExtent)
|
||||
.range([svgHeight - margins.bottom, margins.top]);
|
||||
|
||||
const xScale = d3.scaleLinear()
|
||||
.domain([0, byteFrequency.length - 1])
|
||||
.range([margins.left - binWidth, svgWidth - margins.right]);
|
||||
|
||||
svg.selectAll("rect")
|
||||
.data(byteFrequency)
|
||||
.enter().append("rect")
|
||||
.attr("x", (_, i) => xScale(i) + binWidth)
|
||||
.attr("y", dataPoint => yScale(dataPoint))
|
||||
.attr("width", binWidth)
|
||||
.attr("height", dataPoint => yScale(yExtent[0]) - yScale(dataPoint))
|
||||
.attr("fill", "blue");
|
||||
|
||||
this.createAxes(svg, xScale, yScale, svgHeight, svgWidth, margins, "", "Byte", "Byte Frequency");
|
||||
|
||||
return svg._groups[0][0].outerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a byte frequency histogram
|
||||
*
|
||||
* @param {number[]} entropyData
|
||||
* @returns {HTML}
|
||||
*/
|
||||
createEntropyCurve(entropyData) {
|
||||
const margins = { top: 30, right: 20, bottom: 50, left: 30 };
|
||||
|
||||
const svgWidth = 500,
|
||||
svgHeight = 500;
|
||||
|
||||
const document = new nodom.Document();
|
||||
let svg = document.createElement("svg");
|
||||
svg = d3.select(svg)
|
||||
.attr("width", "100%")
|
||||
.attr("height", "100%")
|
||||
.attr("viewBox", `0 0 ${svgWidth} ${svgHeight}`);
|
||||
|
||||
const yScale = d3.scaleLinear()
|
||||
.domain([0, d3.max(entropyData, d => d)])
|
||||
.range([svgHeight - margins.bottom, margins.top]);
|
||||
|
||||
const xScale = d3.scaleLinear()
|
||||
.domain([0, entropyData.length])
|
||||
.range([margins.left, svgWidth - margins.right]);
|
||||
|
||||
const line = d3.line()
|
||||
.x((_, i) => xScale(i))
|
||||
.y(d => yScale(d))
|
||||
.curve(d3.curveMonotoneX);
|
||||
|
||||
if (entropyData.length > 0) {
|
||||
svg.append("path")
|
||||
.datum(entropyData)
|
||||
.attr("d", line);
|
||||
|
||||
svg.selectAll("path").attr("fill", "none").attr("stroke", "steelblue");
|
||||
}
|
||||
|
||||
this.createAxes(svg, xScale, yScale, svgHeight, svgWidth, margins, "Scanning Entropy", "Block", "Entropy");
|
||||
|
||||
return svg._groups[0][0].outerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an image representation of the entropy
|
||||
*
|
||||
* @param {number[]} entropyData
|
||||
* @returns {HTML}
|
||||
*/
|
||||
createEntropyImage(entropyData) {
|
||||
const svgHeight = 100,
|
||||
svgWidth = 100,
|
||||
cellSize = 1,
|
||||
nodes = [];
|
||||
|
||||
for (let i = 0; i < entropyData.length; i++) {
|
||||
nodes.push({
|
||||
x: i % svgWidth,
|
||||
y: Math.floor(i / svgWidth),
|
||||
entropy: entropyData[i]
|
||||
});
|
||||
}
|
||||
|
||||
const document = new nodom.Document();
|
||||
let svg = document.createElement("svg");
|
||||
svg = d3.select(svg)
|
||||
.attr("width", "100%")
|
||||
.attr("height", "100%")
|
||||
.attr("viewBox", `0 0 ${svgWidth} ${svgHeight}`);
|
||||
|
||||
const greyScale = d3.scaleLinear()
|
||||
.domain([0, d3.max(entropyData, d => d)])
|
||||
.range(["#000000", "#FFFFFF"])
|
||||
.interpolate(d3.interpolateRgb);
|
||||
|
||||
svg
|
||||
.selectAll("rect")
|
||||
.data(nodes)
|
||||
.enter().append("rect")
|
||||
.attr("x", d => d.x * cellSize)
|
||||
.attr("y", d => d.y * cellSize)
|
||||
.attr("width", cellSize)
|
||||
.attr("height", cellSize)
|
||||
.style("fill", d => greyScale(d.entropy));
|
||||
|
||||
return svg._groups[0][0].outerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the entropy as a scale bar for web apps.
|
||||
*
|
||||
* @param {number} entropy
|
||||
* @returns {html}
|
||||
* @returns {HTML}
|
||||
*/
|
||||
present(entropy) {
|
||||
createShannonEntropyVisualization(entropy) {
|
||||
return `Shannon entropy: ${entropy}
|
||||
<br><canvas id='chart-area'></canvas><br>
|
||||
- 0 represents no randomness (i.e. all the bytes in the data have the same value) whereas 8, the maximum, represents a completely random string.
|
||||
- Standard English text usually falls somewhere between 3.5 and 5.
|
||||
- Properly encrypted or compressed data of a reasonable length should have an entropy of over 7.5.
|
||||
<br><canvas id='chart-area'></canvas><br>
|
||||
- 0 represents no randomness (i.e. all the bytes in the data have the same value) whereas 8, the maximum, represents a completely random string.
|
||||
- Standard English text usually falls somewhere between 3.5 and 5.
|
||||
- Properly encrypted or compressed data of a reasonable length should have an entropy of over 7.5.
|
||||
|
||||
The following results show the entropy of chunks of the input data. Chunks with particularly high entropy could suggest encrypted or compressed sections.
|
||||
The following results show the entropy of chunks of the input data. Chunks with particularly high entropy could suggest encrypted or compressed sections.
|
||||
|
||||
<br><script>
|
||||
var canvas = document.getElementById("chart-area"),
|
||||
parentRect = canvas.parentNode.getBoundingClientRect(),
|
||||
entropy = ${entropy},
|
||||
height = parentRect.height * 0.25;
|
||||
<br><script>
|
||||
var canvas = document.getElementById("chart-area"),
|
||||
parentRect = canvas.parentNode.getBoundingClientRect(),
|
||||
entropy = ${entropy},
|
||||
height = parentRect.height * 0.25;
|
||||
|
||||
canvas.width = parentRect.width * 0.95;
|
||||
canvas.height = height > 150 ? 150 : height;
|
||||
canvas.width = parentRect.width * 0.95;
|
||||
canvas.height = height > 150 ? 150 : height;
|
||||
|
||||
CanvasComponents.drawScaleBar(canvas, entropy, 8, [
|
||||
{
|
||||
label: "English text",
|
||||
min: 3.5,
|
||||
max: 5
|
||||
},{
|
||||
label: "Encrypted/compressed",
|
||||
min: 7.5,
|
||||
max: 8
|
||||
}
|
||||
]);
|
||||
</script>`;
|
||||
CanvasComponents.drawScaleBar(canvas, entropy, 8, [
|
||||
{
|
||||
label: "English text",
|
||||
min: 3.5,
|
||||
max: 5
|
||||
},{
|
||||
label: "Encrypted/compressed",
|
||||
min: 7.5,
|
||||
max: 8
|
||||
}
|
||||
]);
|
||||
</script>`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ArrayBuffer} input
|
||||
* @param {Object[]} args
|
||||
* @returns {json}
|
||||
*/
|
||||
run(input, args) {
|
||||
const visualizationType = args[0];
|
||||
input = new Uint8Array(input);
|
||||
|
||||
switch (visualizationType) {
|
||||
case "Histogram (Bar)":
|
||||
case "Histogram (Line)":
|
||||
return this.calculateByteFrequency(input);
|
||||
case "Curve":
|
||||
case "Image":
|
||||
return this.calculateScanningEntropy(input).entropyData;
|
||||
case "Shannon scale":
|
||||
default:
|
||||
return this.calculateShannonEntropy(input);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the entropy in a visualisation for web apps.
|
||||
*
|
||||
* @param {json} entropyData
|
||||
* @param {Object[]} args
|
||||
* @returns {html}
|
||||
*/
|
||||
present(entropyData, args) {
|
||||
const visualizationType = args[0];
|
||||
|
||||
switch (visualizationType) {
|
||||
case "Histogram (Bar)":
|
||||
return this.createByteFrequencyBarHistogram(entropyData);
|
||||
case "Histogram (Line)":
|
||||
return this.createByteFrequencyLineHistogram(entropyData);
|
||||
case "Curve":
|
||||
return this.createEntropyCurve(entropyData);
|
||||
case "Image":
|
||||
return this.createEntropyImage(entropyData);
|
||||
case "Shannon scale":
|
||||
default:
|
||||
return this.createShannonEntropyVisualization(entropyData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default Entropy;
|
||||
|
||||
100
src/core/operations/ExtractFiles.mjs
Normal file
100
src/core/operations/ExtractFiles.mjs
Normal file
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* @author n1474335 [n1474335@gmail.com]
|
||||
* @copyright Crown Copyright 2018
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import Utils from "../Utils";
|
||||
import {scanForFileTypes, extractFile} from "../lib/FileType";
|
||||
import {FILE_SIGNATURES} from "../lib/FileSignatures";
|
||||
|
||||
/**
|
||||
* Extract Files operation
|
||||
*/
|
||||
class ExtractFiles extends Operation {
|
||||
|
||||
/**
|
||||
* ExtractFiles constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Extract Files";
|
||||
this.module = "Default";
|
||||
this.description = "Performs file carving to attempt to extract files from the input.<br><br>This operation is currently capable of carving out the following formats:<ul><li>JPG</li><li>EXE</li><li>ZIP</li><li>PDF</li><li>PNG</li><li>BMP</li><li>FLV</li><li>RTF</li><li>DOCX, PPTX, XLSX</li><li>EPUB</li><li>GZIP</li><li>ZLIB</li><li>ELF, BIN, AXF, O, PRX, SO</li></ul>";
|
||||
this.infoURL = "https://forensicswiki.org/wiki/File_Carving";
|
||||
this.inputType = "ArrayBuffer";
|
||||
this.outputType = "List<File>";
|
||||
this.presentType = "html";
|
||||
this.args = Object.keys(FILE_SIGNATURES).map(cat => {
|
||||
return {
|
||||
name: cat,
|
||||
type: "boolean",
|
||||
value: cat === "Miscellaneous" ? false : true
|
||||
};
|
||||
}).concat([
|
||||
{
|
||||
name: "Ignore failed extractions",
|
||||
type: "boolean",
|
||||
value: "true"
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ArrayBuffer} input
|
||||
* @param {Object[]} args
|
||||
* @returns {List<File>}
|
||||
*/
|
||||
run(input, args) {
|
||||
const bytes = new Uint8Array(input),
|
||||
categories = [],
|
||||
ignoreFailedExtractions = args.pop(1);
|
||||
|
||||
args.forEach((cat, i) => {
|
||||
if (cat) categories.push(Object.keys(FILE_SIGNATURES)[i]);
|
||||
});
|
||||
|
||||
// Scan for embedded files
|
||||
const detectedFiles = scanForFileTypes(bytes, categories);
|
||||
|
||||
// Extract each file that we support
|
||||
const files = [];
|
||||
const errors = [];
|
||||
detectedFiles.forEach(detectedFile => {
|
||||
try {
|
||||
files.push(extractFile(bytes, detectedFile.fileDetails, detectedFile.offset));
|
||||
} catch (err) {
|
||||
if (!ignoreFailedExtractions && err.message.indexOf("No extraction algorithm available") < 0) {
|
||||
errors.push(
|
||||
`Error while attempting to extract ${detectedFile.fileDetails.name} ` +
|
||||
`at offset ${detectedFile.offset}:\n` +
|
||||
`${err.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (errors.length) {
|
||||
throw new OperationError(errors.join("\n\n"));
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Displays the files in HTML for web apps.
|
||||
*
|
||||
* @param {File[]} files
|
||||
* @returns {html}
|
||||
*/
|
||||
async present(files) {
|
||||
return await Utils.displayFilesAsHTML(files);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default ExtractFiles;
|
||||
94
src/core/operations/FlipImage.mjs
Normal file
94
src/core/operations/FlipImage.mjs
Normal file
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* @author j433866 [j433866@gmail.com]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import { isImage } from "../lib/FileType";
|
||||
import { toBase64 } from "../lib/Base64";
|
||||
import jimp from "jimp";
|
||||
|
||||
/**
|
||||
* Flip Image operation
|
||||
*/
|
||||
class FlipImage extends Operation {
|
||||
|
||||
/**
|
||||
* FlipImage constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Flip Image";
|
||||
this.module = "Image";
|
||||
this.description = "Flips an image along its X or Y axis.";
|
||||
this.infoURL = "";
|
||||
this.inputType = "byteArray";
|
||||
this.outputType = "byteArray";
|
||||
this.presentType = "html";
|
||||
this.args = [
|
||||
{
|
||||
name: "Axis",
|
||||
type: "option",
|
||||
value: ["Horizontal", "Vertical"]
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {byteArray} input
|
||||
* @param {Object[]} args
|
||||
* @returns {byteArray}
|
||||
*/
|
||||
async run(input, args) {
|
||||
const [flipAxis] = args;
|
||||
if (!isImage(input)) {
|
||||
throw new OperationError("Invalid input file type.");
|
||||
}
|
||||
|
||||
let image;
|
||||
try {
|
||||
image = await jimp.read(Buffer.from(input));
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error loading image. (${err})`);
|
||||
}
|
||||
try {
|
||||
if (ENVIRONMENT_IS_WORKER())
|
||||
self.sendStatusMessage("Flipping image...");
|
||||
switch (flipAxis){
|
||||
case "Horizontal":
|
||||
image.flip(true, false);
|
||||
break;
|
||||
case "Vertical":
|
||||
image.flip(false, true);
|
||||
break;
|
||||
}
|
||||
|
||||
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
|
||||
return [...imageBuffer];
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error flipping image. (${err})`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the flipped image using HTML for web apps
|
||||
* @param {byteArray} data
|
||||
* @returns {html}
|
||||
*/
|
||||
present(data) {
|
||||
if (!data.length) return "";
|
||||
|
||||
const type = isImage(data);
|
||||
if (!type) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
return `<img src="data:${type};base64,${toBase64(data)}">`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default FlipImage;
|
||||
@@ -89,7 +89,7 @@ class Fork extends Operation {
|
||||
// Run recipe over each tranche
|
||||
for (i = 0; i < inputs.length; i++) {
|
||||
// Baseline ing values for each tranche so that registers are reset
|
||||
subOpList.forEach((op, i) => {
|
||||
recipe.opList.forEach((op, i) => {
|
||||
op.ingValues = JSON.parse(JSON.stringify(ingValues[i]));
|
||||
});
|
||||
|
||||
|
||||
@@ -28,6 +28,8 @@ import Fletcher64Checksum from "./Fletcher64Checksum";
|
||||
import Adler32Checksum from "./Adler32Checksum";
|
||||
import CRC16Checksum from "./CRC16Checksum";
|
||||
import CRC32Checksum from "./CRC32Checksum";
|
||||
import BLAKE2b from "./BLAKE2b";
|
||||
import BLAKE2s from "./BLAKE2s";
|
||||
|
||||
/**
|
||||
* Generate all hashes operation
|
||||
@@ -86,6 +88,14 @@ class GenerateAllHashes extends Operation {
|
||||
"\nWhirlpool-0: " + (new Whirlpool()).run(arrayBuffer, ["Whirlpool-0"]) +
|
||||
"\nWhirlpool-T: " + (new Whirlpool()).run(arrayBuffer, ["Whirlpool-T"]) +
|
||||
"\nWhirlpool: " + (new Whirlpool()).run(arrayBuffer, ["Whirlpool"]) +
|
||||
"\nBLAKE2b-128: " + (new BLAKE2b).run(arrayBuffer, ["128", "Hex", {string: "", option: "UTF8"}]) +
|
||||
"\nBLAKE2b-160: " + (new BLAKE2b).run(arrayBuffer, ["160", "Hex", {string: "", option: "UTF8"}]) +
|
||||
"\nBLAKE2b-256: " + (new BLAKE2b).run(arrayBuffer, ["256", "Hex", {string: "", option: "UTF8"}]) +
|
||||
"\nBLAKE2b-384: " + (new BLAKE2b).run(arrayBuffer, ["384", "Hex", {string: "", option: "UTF8"}]) +
|
||||
"\nBLAKE2b-512: " + (new BLAKE2b).run(arrayBuffer, ["512", "Hex", {string: "", option: "UTF8"}]) +
|
||||
"\nBLAKE2s-128: " + (new BLAKE2s).run(arrayBuffer, ["128", "Hex", {string: "", option: "UTF8"}]) +
|
||||
"\nBLAKE2s-160: " + (new BLAKE2s).run(arrayBuffer, ["160", "Hex", {string: "", option: "UTF8"}]) +
|
||||
"\nBLAKE2s-256: " + (new BLAKE2s).run(arrayBuffer, ["256", "Hex", {string: "", option: "UTF8"}]) +
|
||||
"\nSSDEEP: " + (new SSDEEP()).run(str) +
|
||||
"\nCTPH: " + (new CTPH()).run(str) +
|
||||
"\n\nChecksums:" +
|
||||
|
||||
@@ -8,7 +8,7 @@ import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import qr from "qr-image";
|
||||
import { toBase64 } from "../lib/Base64";
|
||||
import Magic from "../lib/Magic";
|
||||
import { isImage } from "../lib/FileType";
|
||||
import Utils from "../Utils";
|
||||
|
||||
/**
|
||||
@@ -100,9 +100,9 @@ class GenerateQRCode extends Operation {
|
||||
|
||||
if (format === "PNG") {
|
||||
let dataURI = "data:";
|
||||
const type = Magic.magicFileType(data);
|
||||
if (type && type.mime.indexOf("image") === 0){
|
||||
dataURI += type.mime + ";";
|
||||
const mime = isImage(data);
|
||||
if (mime){
|
||||
dataURI += mime + ";";
|
||||
} else {
|
||||
throw new OperationError("Invalid PNG file generated by QR image");
|
||||
}
|
||||
|
||||
41
src/core/operations/HTMLToText.mjs
Normal file
41
src/core/operations/HTMLToText.mjs
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* @author tlwr [toby@toby.codes]
|
||||
* @author Matt C [me@mitt.dev]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
|
||||
/**
|
||||
* HTML To Text operation
|
||||
*/
|
||||
class HTMLToText extends Operation {
|
||||
|
||||
/**
|
||||
* HTMLToText constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "HTML To Text";
|
||||
this.module = "Default";
|
||||
this.description = "Converts an HTML output from an operation to a readable string instead of being rendered in the DOM.";
|
||||
this.infoURL = "";
|
||||
this.inputType = "html";
|
||||
this.outputType = "string";
|
||||
this.args = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {html} input
|
||||
* @param {Object[]} args
|
||||
* @returns {string}
|
||||
*/
|
||||
run(input, args) {
|
||||
return input;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default HTMLToText;
|
||||
266
src/core/operations/HeatmapChart.mjs
Normal file
266
src/core/operations/HeatmapChart.mjs
Normal file
@@ -0,0 +1,266 @@
|
||||
/**
|
||||
* @author tlwr [toby@toby.codes]
|
||||
* @author Matt C [me@mitt.dev]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import * as d3temp from "d3";
|
||||
import * as nodomtemp from "nodom";
|
||||
import { getScatterValues, RECORD_DELIMITER_OPTIONS, COLOURS, FIELD_DELIMITER_OPTIONS } from "../lib/Charts";
|
||||
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import Utils from "../Utils";
|
||||
|
||||
const d3 = d3temp.default ? d3temp.default : d3temp;
|
||||
const nodom = nodomtemp.default ? nodomtemp.default: nodomtemp;
|
||||
|
||||
/**
|
||||
* Heatmap chart operation
|
||||
*/
|
||||
class HeatmapChart extends Operation {
|
||||
|
||||
/**
|
||||
* HeatmapChart constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Heatmap chart";
|
||||
this.module = "Charts";
|
||||
this.description = "A heatmap is a graphical representation of data where the individual values contained in a matrix are represented as colors.";
|
||||
this.infoURL = "https://wikipedia.org/wiki/Heat_map";
|
||||
this.inputType = "string";
|
||||
this.outputType = "html";
|
||||
this.args = [
|
||||
{
|
||||
name: "Record delimiter",
|
||||
type: "option",
|
||||
value: RECORD_DELIMITER_OPTIONS,
|
||||
},
|
||||
{
|
||||
name: "Field delimiter",
|
||||
type: "option",
|
||||
value: FIELD_DELIMITER_OPTIONS,
|
||||
},
|
||||
{
|
||||
name: "Number of vertical bins",
|
||||
type: "number",
|
||||
value: 25,
|
||||
},
|
||||
{
|
||||
name: "Number of horizontal bins",
|
||||
type: "number",
|
||||
value: 25,
|
||||
},
|
||||
{
|
||||
name: "Use column headers as labels",
|
||||
type: "boolean",
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
name: "X label",
|
||||
type: "string",
|
||||
value: "",
|
||||
},
|
||||
{
|
||||
name: "Y label",
|
||||
type: "string",
|
||||
value: "",
|
||||
},
|
||||
{
|
||||
name: "Draw bin edges",
|
||||
type: "boolean",
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
name: "Min colour value",
|
||||
type: "string",
|
||||
value: COLOURS.min,
|
||||
},
|
||||
{
|
||||
name: "Max colour value",
|
||||
type: "string",
|
||||
value: COLOURS.max,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Heatmap chart operation.
|
||||
*
|
||||
* @param {string} input
|
||||
* @param {Object[]} args
|
||||
* @returns {html}
|
||||
*/
|
||||
run(input, args) {
|
||||
const recordDelimiter = Utils.charRep(args[0]),
|
||||
fieldDelimiter = Utils.charRep(args[1]),
|
||||
vBins = args[2],
|
||||
hBins = args[3],
|
||||
columnHeadingsAreIncluded = args[4],
|
||||
drawEdges = args[7],
|
||||
minColour = args[8],
|
||||
maxColour = args[9],
|
||||
dimension = 500;
|
||||
if (vBins <= 0) throw new OperationError("Number of vertical bins must be greater than 0");
|
||||
if (hBins <= 0) throw new OperationError("Number of horizontal bins must be greater than 0");
|
||||
|
||||
let xLabel = args[5],
|
||||
yLabel = args[6];
|
||||
const { headings, values } = getScatterValues(
|
||||
input,
|
||||
recordDelimiter,
|
||||
fieldDelimiter,
|
||||
columnHeadingsAreIncluded
|
||||
);
|
||||
|
||||
if (headings) {
|
||||
xLabel = headings.x;
|
||||
yLabel = headings.y;
|
||||
}
|
||||
|
||||
const document = new nodom.Document();
|
||||
let svg = document.createElement("svg");
|
||||
svg = d3.select(svg)
|
||||
.attr("width", "100%")
|
||||
.attr("height", "100%")
|
||||
.attr("viewBox", `0 0 ${dimension} ${dimension}`);
|
||||
|
||||
const margin = {
|
||||
top: 10,
|
||||
right: 0,
|
||||
bottom: 40,
|
||||
left: 30,
|
||||
},
|
||||
width = dimension - margin.left - margin.right,
|
||||
height = dimension - margin.top - margin.bottom,
|
||||
binWidth = width / hBins,
|
||||
binHeight = height/ vBins,
|
||||
marginedSpace = svg.append("g")
|
||||
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
|
||||
|
||||
const bins = this.getHeatmapPacking(values, vBins, hBins),
|
||||
maxCount = Math.max(...bins.map(row => {
|
||||
const lengths = row.map(cell => cell.length);
|
||||
return Math.max(...lengths);
|
||||
}));
|
||||
|
||||
const xExtent = d3.extent(values, d => d[0]),
|
||||
yExtent = d3.extent(values, d => d[1]);
|
||||
|
||||
const xAxis = d3.scaleLinear()
|
||||
.domain(xExtent)
|
||||
.range([0, width]);
|
||||
const yAxis = d3.scaleLinear()
|
||||
.domain(yExtent)
|
||||
.range([height, 0]);
|
||||
|
||||
const colour = d3.scaleSequential(d3.interpolateLab(minColour, maxColour))
|
||||
.domain([0, maxCount]);
|
||||
|
||||
marginedSpace.append("clipPath")
|
||||
.attr("id", "clip")
|
||||
.append("rect")
|
||||
.attr("width", width)
|
||||
.attr("height", height);
|
||||
|
||||
marginedSpace.append("g")
|
||||
.attr("class", "bins")
|
||||
.attr("clip-path", "url(#clip)")
|
||||
.selectAll("g")
|
||||
.data(bins)
|
||||
.enter()
|
||||
.append("g")
|
||||
.selectAll("rect")
|
||||
.data(d => d)
|
||||
.enter()
|
||||
.append("rect")
|
||||
.attr("x", (d) => binWidth * d.x)
|
||||
.attr("y", (d) => (height - binHeight * (d.y + 1)))
|
||||
.attr("width", binWidth)
|
||||
.attr("height", binHeight)
|
||||
.attr("fill", (d) => colour(d.length))
|
||||
.attr("stroke", drawEdges ? "rgba(0, 0, 0, 0.5)" : "none")
|
||||
.attr("stroke-width", drawEdges ? "0.5" : "none")
|
||||
.append("title")
|
||||
.text(d => {
|
||||
const count = d.length,
|
||||
perc = 100.0 * d.length / values.length,
|
||||
tooltip = `Count: ${count}\n
|
||||
Percentage: ${perc.toFixed(2)}%\n
|
||||
`.replace(/\s{2,}/g, "\n");
|
||||
return tooltip;
|
||||
});
|
||||
|
||||
marginedSpace.append("g")
|
||||
.attr("class", "axis axis--y")
|
||||
.call(d3.axisLeft(yAxis).tickSizeOuter(-width));
|
||||
|
||||
svg.append("text")
|
||||
.attr("transform", "rotate(-90)")
|
||||
.attr("y", -margin.left)
|
||||
.attr("x", -(height / 2))
|
||||
.attr("dy", "1em")
|
||||
.style("text-anchor", "middle")
|
||||
.text(yLabel);
|
||||
|
||||
marginedSpace.append("g")
|
||||
.attr("class", "axis axis--x")
|
||||
.attr("transform", "translate(0," + height + ")")
|
||||
.call(d3.axisBottom(xAxis).tickSizeOuter(-height));
|
||||
|
||||
svg.append("text")
|
||||
.attr("x", width / 2)
|
||||
.attr("y", dimension)
|
||||
.style("text-anchor", "middle")
|
||||
.text(xLabel);
|
||||
|
||||
return svg._groups[0][0].outerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Packs a list of x, y coordinates into a number of bins for use in a heatmap.
|
||||
*
|
||||
* @param {Object[]} points
|
||||
* @param {number} number of vertical bins
|
||||
* @param {number} number of horizontal bins
|
||||
* @returns {Object[]} a list of bins (each bin is an Array) with x y coordinates, filled with the points
|
||||
*/
|
||||
getHeatmapPacking(values, vBins, hBins) {
|
||||
const xBounds = d3.extent(values, d => d[0]),
|
||||
yBounds = d3.extent(values, d => d[1]),
|
||||
bins = [];
|
||||
|
||||
if (xBounds[0] === xBounds[1]) throw "Cannot pack points. There is no difference between the minimum and maximum X coordinate.";
|
||||
if (yBounds[0] === yBounds[1]) throw "Cannot pack points. There is no difference between the minimum and maximum Y coordinate.";
|
||||
|
||||
for (let y = 0; y < vBins; y++) {
|
||||
bins.push([]);
|
||||
for (let x = 0; x < hBins; x++) {
|
||||
const item = [];
|
||||
item.y = y;
|
||||
item.x = x;
|
||||
|
||||
bins[y].push(item);
|
||||
} // x
|
||||
} // y
|
||||
|
||||
const epsilon = 0.000000001; // This is to clamp values that are exactly the maximum;
|
||||
|
||||
values.forEach(v => {
|
||||
const fractionOfY = (v[1] - yBounds[0]) / ((yBounds[1] + epsilon) - yBounds[0]),
|
||||
fractionOfX = (v[0] - xBounds[0]) / ((xBounds[1] + epsilon) - xBounds[0]),
|
||||
y = Math.floor(vBins * fractionOfY),
|
||||
x = Math.floor(hBins * fractionOfX);
|
||||
|
||||
bins[y][x].push({x: v[0], y: v[1]});
|
||||
});
|
||||
|
||||
return bins;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default HeatmapChart;
|
||||
296
src/core/operations/HexDensityChart.mjs
Normal file
296
src/core/operations/HexDensityChart.mjs
Normal file
@@ -0,0 +1,296 @@
|
||||
/**
|
||||
* @author tlwr [toby@toby.codes]
|
||||
* @author Matt C [me@mitt.dev]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import * as d3temp from "d3";
|
||||
import * as d3hexbintemp from "d3-hexbin";
|
||||
import * as nodomtemp from "nodom";
|
||||
import { getScatterValues, RECORD_DELIMITER_OPTIONS, COLOURS, FIELD_DELIMITER_OPTIONS } from "../lib/Charts";
|
||||
|
||||
import Operation from "../Operation";
|
||||
import Utils from "../Utils";
|
||||
|
||||
const d3 = d3temp.default ? d3temp.default : d3temp;
|
||||
const d3hexbin = d3hexbintemp.default ? d3hexbintemp.default : d3hexbintemp;
|
||||
const nodom = nodomtemp.default ? nodomtemp.default: nodomtemp;
|
||||
|
||||
|
||||
/**
|
||||
* Hex Density chart operation
|
||||
*/
|
||||
class HexDensityChart extends Operation {
|
||||
|
||||
/**
|
||||
* HexDensityChart constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Hex Density chart";
|
||||
this.module = "Charts";
|
||||
this.description = "Hex density charts are used in a similar way to scatter charts, however rather than rendering tens of thousands of points, it groups the points into a few hundred hexagons to show the distribution.";
|
||||
this.inputType = "string";
|
||||
this.outputType = "html";
|
||||
this.args = [
|
||||
{
|
||||
name: "Record delimiter",
|
||||
type: "option",
|
||||
value: RECORD_DELIMITER_OPTIONS,
|
||||
},
|
||||
{
|
||||
name: "Field delimiter",
|
||||
type: "option",
|
||||
value: FIELD_DELIMITER_OPTIONS,
|
||||
},
|
||||
{
|
||||
name: "Pack radius",
|
||||
type: "number",
|
||||
value: 25,
|
||||
},
|
||||
{
|
||||
name: "Draw radius",
|
||||
type: "number",
|
||||
value: 15,
|
||||
},
|
||||
{
|
||||
name: "Use column headers as labels",
|
||||
type: "boolean",
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
name: "X label",
|
||||
type: "string",
|
||||
value: "",
|
||||
},
|
||||
{
|
||||
name: "Y label",
|
||||
type: "string",
|
||||
value: "",
|
||||
},
|
||||
{
|
||||
name: "Draw hexagon edges",
|
||||
type: "boolean",
|
||||
value: false,
|
||||
},
|
||||
{
|
||||
name: "Min colour value",
|
||||
type: "string",
|
||||
value: COLOURS.min,
|
||||
},
|
||||
{
|
||||
name: "Max colour value",
|
||||
type: "string",
|
||||
value: COLOURS.max,
|
||||
},
|
||||
{
|
||||
name: "Draw empty hexagons within data boundaries",
|
||||
type: "boolean",
|
||||
value: false,
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Hex Bin chart operation.
|
||||
*
|
||||
* @param {string} input
|
||||
* @param {Object[]} args
|
||||
* @returns {html}
|
||||
*/
|
||||
run(input, args) {
|
||||
const recordDelimiter = Utils.charRep(args[0]),
|
||||
fieldDelimiter = Utils.charRep(args[1]),
|
||||
packRadius = args[2],
|
||||
drawRadius = args[3],
|
||||
columnHeadingsAreIncluded = args[4],
|
||||
drawEdges = args[7],
|
||||
minColour = args[8],
|
||||
maxColour = args[9],
|
||||
drawEmptyHexagons = args[10],
|
||||
dimension = 500;
|
||||
|
||||
let xLabel = args[5],
|
||||
yLabel = args[6];
|
||||
const { headings, values } = getScatterValues(
|
||||
input,
|
||||
recordDelimiter,
|
||||
fieldDelimiter,
|
||||
columnHeadingsAreIncluded
|
||||
);
|
||||
|
||||
if (headings) {
|
||||
xLabel = headings.x;
|
||||
yLabel = headings.y;
|
||||
}
|
||||
|
||||
const document = new nodom.Document();
|
||||
let svg = document.createElement("svg");
|
||||
svg = d3.select(svg)
|
||||
.attr("width", "100%")
|
||||
.attr("height", "100%")
|
||||
.attr("viewBox", `0 0 ${dimension} ${dimension}`);
|
||||
|
||||
const margin = {
|
||||
top: 10,
|
||||
right: 0,
|
||||
bottom: 40,
|
||||
left: 30,
|
||||
},
|
||||
width = dimension - margin.left - margin.right,
|
||||
height = dimension - margin.top - margin.bottom,
|
||||
marginedSpace = svg.append("g")
|
||||
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
|
||||
|
||||
const hexbin = d3hexbin.hexbin()
|
||||
.radius(packRadius)
|
||||
.extent([0, 0], [width, height]);
|
||||
|
||||
const hexPoints = hexbin(values),
|
||||
maxCount = Math.max(...hexPoints.map(b => b.length));
|
||||
|
||||
const xExtent = d3.extent(hexPoints, d => d.x),
|
||||
yExtent = d3.extent(hexPoints, d => d.y);
|
||||
xExtent[0] -= 2 * packRadius;
|
||||
xExtent[1] += 3 * packRadius;
|
||||
yExtent[0] -= 2 * packRadius;
|
||||
yExtent[1] += 2 * packRadius;
|
||||
|
||||
const xAxis = d3.scaleLinear()
|
||||
.domain(xExtent)
|
||||
.range([0, width]);
|
||||
const yAxis = d3.scaleLinear()
|
||||
.domain(yExtent)
|
||||
.range([height, 0]);
|
||||
|
||||
const colour = d3.scaleSequential(d3.interpolateLab(minColour, maxColour))
|
||||
.domain([0, maxCount]);
|
||||
|
||||
marginedSpace.append("clipPath")
|
||||
.attr("id", "clip")
|
||||
.append("rect")
|
||||
.attr("width", width)
|
||||
.attr("height", height);
|
||||
|
||||
if (drawEmptyHexagons) {
|
||||
marginedSpace.append("g")
|
||||
.attr("class", "empty-hexagon")
|
||||
.selectAll("path")
|
||||
.data(this.getEmptyHexagons(hexPoints, packRadius))
|
||||
.enter()
|
||||
.append("path")
|
||||
.attr("d", d => {
|
||||
return `M${xAxis(d.x)},${yAxis(d.y)} ${hexbin.hexagon(drawRadius)}`;
|
||||
})
|
||||
.attr("fill", (d) => colour(0))
|
||||
.attr("stroke", drawEdges ? "black" : "none")
|
||||
.attr("stroke-width", drawEdges ? "0.5" : "none")
|
||||
.append("title")
|
||||
.text(d => {
|
||||
const count = 0,
|
||||
perc = 0,
|
||||
tooltip = `Count: ${count}\n
|
||||
Percentage: ${perc.toFixed(2)}%\n
|
||||
Center: ${d.x.toFixed(2)}, ${d.y.toFixed(2)}\n
|
||||
`.replace(/\s{2,}/g, "\n");
|
||||
return tooltip;
|
||||
});
|
||||
}
|
||||
|
||||
marginedSpace.append("g")
|
||||
.attr("class", "hexagon")
|
||||
.attr("clip-path", "url(#clip)")
|
||||
.selectAll("path")
|
||||
.data(hexPoints)
|
||||
.enter()
|
||||
.append("path")
|
||||
.attr("d", d => {
|
||||
return `M${xAxis(d.x)},${yAxis(d.y)} ${hexbin.hexagon(drawRadius)}`;
|
||||
})
|
||||
.attr("fill", (d) => colour(d.length))
|
||||
.attr("stroke", drawEdges ? "black" : "none")
|
||||
.attr("stroke-width", drawEdges ? "0.5" : "none")
|
||||
.append("title")
|
||||
.text(d => {
|
||||
const count = d.length,
|
||||
perc = 100.0 * d.length / values.length,
|
||||
CX = d.x,
|
||||
CY = d.y,
|
||||
xMin = Math.min(...d.map(d => d[0])),
|
||||
xMax = Math.max(...d.map(d => d[0])),
|
||||
yMin = Math.min(...d.map(d => d[1])),
|
||||
yMax = Math.max(...d.map(d => d[1])),
|
||||
tooltip = `Count: ${count}\n
|
||||
Percentage: ${perc.toFixed(2)}%\n
|
||||
Center: ${CX.toFixed(2)}, ${CY.toFixed(2)}\n
|
||||
Min X: ${xMin.toFixed(2)}\n
|
||||
Max X: ${xMax.toFixed(2)}\n
|
||||
Min Y: ${yMin.toFixed(2)}\n
|
||||
Max Y: ${yMax.toFixed(2)}
|
||||
`.replace(/\s{2,}/g, "\n");
|
||||
return tooltip;
|
||||
});
|
||||
|
||||
marginedSpace.append("g")
|
||||
.attr("class", "axis axis--y")
|
||||
.call(d3.axisLeft(yAxis).tickSizeOuter(-width));
|
||||
|
||||
svg.append("text")
|
||||
.attr("transform", "rotate(-90)")
|
||||
.attr("y", -margin.left)
|
||||
.attr("x", -(height / 2))
|
||||
.attr("dy", "1em")
|
||||
.style("text-anchor", "middle")
|
||||
.text(yLabel);
|
||||
|
||||
marginedSpace.append("g")
|
||||
.attr("class", "axis axis--x")
|
||||
.attr("transform", "translate(0," + height + ")")
|
||||
.call(d3.axisBottom(xAxis).tickSizeOuter(-height));
|
||||
|
||||
svg.append("text")
|
||||
.attr("x", width / 2)
|
||||
.attr("y", dimension)
|
||||
.style("text-anchor", "middle")
|
||||
.text(xLabel);
|
||||
|
||||
return svg._groups[0][0].outerHTML;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Hex Bin chart operation.
|
||||
*
|
||||
* @param {Object[]} - centres
|
||||
* @param {number} - radius
|
||||
* @returns {Object[]}
|
||||
*/
|
||||
getEmptyHexagons(centres, radius) {
|
||||
const emptyCentres = [],
|
||||
boundingRect = [d3.extent(centres, d => d.x), d3.extent(centres, d => d.y)],
|
||||
hexagonCenterToEdge = Math.cos(2 * Math.PI / 12) * radius,
|
||||
hexagonEdgeLength = Math.sin(2 * Math.PI / 12) * radius;
|
||||
let indent = false;
|
||||
|
||||
for (let y = boundingRect[1][0]; y <= boundingRect[1][1] + radius; y += hexagonEdgeLength + radius) {
|
||||
for (let x = boundingRect[0][0]; x <= boundingRect[0][1] + radius; x += 2 * hexagonCenterToEdge) {
|
||||
let cx = x;
|
||||
const cy = y;
|
||||
|
||||
if (indent && x >= boundingRect[0][1]) break;
|
||||
if (indent) cx += hexagonCenterToEdge;
|
||||
|
||||
emptyCentres.push({x: cx, y: cy});
|
||||
}
|
||||
indent = !indent;
|
||||
}
|
||||
|
||||
return emptyCentres;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default HexDensityChart;
|
||||
103
src/core/operations/ImageBrightnessContrast.mjs
Normal file
103
src/core/operations/ImageBrightnessContrast.mjs
Normal file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* @author j433866 [j433866@gmail.com]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import { isImage } from "../lib/FileType";
|
||||
import { toBase64 } from "../lib/Base64.mjs";
|
||||
import jimp from "jimp";
|
||||
|
||||
/**
|
||||
* Image Brightness / Contrast operation
|
||||
*/
|
||||
class ImageBrightnessContrast extends Operation {
|
||||
|
||||
/**
|
||||
* ImageBrightnessContrast constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Image Brightness / Contrast";
|
||||
this.module = "Image";
|
||||
this.description = "Adjust the brightness or contrast of an image.";
|
||||
this.infoURL = "";
|
||||
this.inputType = "byteArray";
|
||||
this.outputType = "byteArray";
|
||||
this.presentType = "html";
|
||||
this.args = [
|
||||
{
|
||||
name: "Brightness",
|
||||
type: "number",
|
||||
value: 0,
|
||||
min: -100,
|
||||
max: 100
|
||||
},
|
||||
{
|
||||
name: "Contrast",
|
||||
type: "number",
|
||||
value: 0,
|
||||
min: -100,
|
||||
max: 100
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {byteArray} input
|
||||
* @param {Object[]} args
|
||||
* @returns {byteArray}
|
||||
*/
|
||||
async run(input, args) {
|
||||
const [brightness, contrast] = args;
|
||||
if (!isImage(input)) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
let image;
|
||||
try {
|
||||
image = await jimp.read(Buffer.from(input));
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error loading image. (${err})`);
|
||||
}
|
||||
try {
|
||||
if (brightness !== 0) {
|
||||
if (ENVIRONMENT_IS_WORKER())
|
||||
self.sendStatusMessage("Changing image brightness...");
|
||||
image.brightness(brightness / 100);
|
||||
}
|
||||
if (contrast !== 0) {
|
||||
if (ENVIRONMENT_IS_WORKER())
|
||||
self.sendStatusMessage("Changing image contrast...");
|
||||
image.contrast(contrast / 100);
|
||||
}
|
||||
|
||||
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
|
||||
return [...imageBuffer];
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error adjusting image brightness or contrast. (${err})`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the image using HTML for web apps
|
||||
* @param {byteArray} data
|
||||
* @returns {html}
|
||||
*/
|
||||
present(data) {
|
||||
if (!data.length) return "";
|
||||
|
||||
const type = isImage(data);
|
||||
if (!type) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
return `<img src="data:${type};base64,${toBase64(data)}">`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default ImageBrightnessContrast;
|
||||
94
src/core/operations/ImageFilter.mjs
Normal file
94
src/core/operations/ImageFilter.mjs
Normal file
@@ -0,0 +1,94 @@
|
||||
/**
|
||||
* @author j433866 [j433866@gmail.com]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import { isImage } from "../lib/FileType";
|
||||
import { toBase64 } from "../lib/Base64.mjs";
|
||||
import jimp from "jimp";
|
||||
|
||||
/**
|
||||
* Image Filter operation
|
||||
*/
|
||||
class ImageFilter extends Operation {
|
||||
|
||||
/**
|
||||
* ImageFilter constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Image Filter";
|
||||
this.module = "Image";
|
||||
this.description = "Applies a greyscale or sepia filter to an image.";
|
||||
this.infoURL = "";
|
||||
this.inputType = "byteArray";
|
||||
this.outputType = "byteArray";
|
||||
this.presentType = "html";
|
||||
this.args = [
|
||||
{
|
||||
name: "Filter type",
|
||||
type: "option",
|
||||
value: [
|
||||
"Greyscale",
|
||||
"Sepia"
|
||||
]
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {byteArray} input
|
||||
* @param {Object[]} args
|
||||
* @returns {byteArray}
|
||||
*/
|
||||
async run(input, args) {
|
||||
const [filterType] = args;
|
||||
if (!isImage(input)){
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
let image;
|
||||
try {
|
||||
image = await jimp.read(Buffer.from(input));
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error loading image. (${err})`);
|
||||
}
|
||||
try {
|
||||
if (ENVIRONMENT_IS_WORKER())
|
||||
self.sendStatusMessage("Applying " + filterType.toLowerCase() + " filter to image...");
|
||||
if (filterType === "Greyscale") {
|
||||
image.greyscale();
|
||||
} else {
|
||||
image.sepia();
|
||||
}
|
||||
|
||||
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
|
||||
return [...imageBuffer];
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error applying filter to image. (${err})`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the blurred image using HTML for web apps
|
||||
* @param {byteArray} data
|
||||
* @returns {html}
|
||||
*/
|
||||
present(data) {
|
||||
if (!data.length) return "";
|
||||
|
||||
const type = isImage(data);
|
||||
if (!type) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
return `<img src="data:${type};base64,${toBase64(data)}">`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default ImageFilter;
|
||||
129
src/core/operations/ImageHueSaturationLightness.mjs
Normal file
129
src/core/operations/ImageHueSaturationLightness.mjs
Normal file
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* @author j433866 [j433866@gmail.com]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import { isImage } from "../lib/FileType";
|
||||
import { toBase64 } from "../lib/Base64.mjs";
|
||||
import jimp from "jimp";
|
||||
|
||||
/**
|
||||
* Image Hue/Saturation/Lightness operation
|
||||
*/
|
||||
class ImageHueSaturationLightness extends Operation {
|
||||
|
||||
/**
|
||||
* ImageHueSaturationLightness constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Image Hue/Saturation/Lightness";
|
||||
this.module = "Image";
|
||||
this.description = "Adjusts the hue / saturation / lightness (HSL) values of an image.";
|
||||
this.infoURL = "";
|
||||
this.inputType = "byteArray";
|
||||
this.outputType = "byteArray";
|
||||
this.presentType = "html";
|
||||
this.args = [
|
||||
{
|
||||
name: "Hue",
|
||||
type: "number",
|
||||
value: 0,
|
||||
min: -360,
|
||||
max: 360
|
||||
},
|
||||
{
|
||||
name: "Saturation",
|
||||
type: "number",
|
||||
value: 0,
|
||||
min: -100,
|
||||
max: 100
|
||||
},
|
||||
{
|
||||
name: "Lightness",
|
||||
type: "number",
|
||||
value: 0,
|
||||
min: -100,
|
||||
max: 100
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {byteArray} input
|
||||
* @param {Object[]} args
|
||||
* @returns {byteArray}
|
||||
*/
|
||||
async run(input, args) {
|
||||
const [hue, saturation, lightness] = args;
|
||||
|
||||
if (!isImage(input)) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
let image;
|
||||
try {
|
||||
image = await jimp.read(Buffer.from(input));
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error loading image. (${err})`);
|
||||
}
|
||||
try {
|
||||
if (hue !== 0) {
|
||||
if (ENVIRONMENT_IS_WORKER())
|
||||
self.sendStatusMessage("Changing image hue...");
|
||||
image.colour([
|
||||
{
|
||||
apply: "hue",
|
||||
params: [hue]
|
||||
}
|
||||
]);
|
||||
}
|
||||
if (saturation !== 0) {
|
||||
if (ENVIRONMENT_IS_WORKER())
|
||||
self.sendStatusMessage("Changing image saturation...");
|
||||
image.colour([
|
||||
{
|
||||
apply: "saturate",
|
||||
params: [saturation]
|
||||
}
|
||||
]);
|
||||
}
|
||||
if (lightness !== 0) {
|
||||
if (ENVIRONMENT_IS_WORKER())
|
||||
self.sendStatusMessage("Changing image lightness...");
|
||||
image.colour([
|
||||
{
|
||||
apply: "lighten",
|
||||
params: [lightness]
|
||||
}
|
||||
]);
|
||||
}
|
||||
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
|
||||
return [...imageBuffer];
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error adjusting image hue / saturation / lightness. (${err})`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the image using HTML for web apps
|
||||
* @param {byteArray} data
|
||||
* @returns {html}
|
||||
*/
|
||||
present(data) {
|
||||
if (!data.length) return "";
|
||||
|
||||
const type = isImage(data);
|
||||
if (!type) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
return `<img src="data:${type};base64,${toBase64(data)}">`;
|
||||
}
|
||||
}
|
||||
|
||||
export default ImageHueSaturationLightness;
|
||||
89
src/core/operations/ImageOpacity.mjs
Normal file
89
src/core/operations/ImageOpacity.mjs
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* @author j433866 [j433866@gmail.com]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import { isImage } from "../lib/FileType";
|
||||
import { toBase64 } from "../lib/Base64.mjs";
|
||||
import jimp from "jimp";
|
||||
|
||||
/**
|
||||
* Image Opacity operation
|
||||
*/
|
||||
class ImageOpacity extends Operation {
|
||||
|
||||
/**
|
||||
* ImageOpacity constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Image Opacity";
|
||||
this.module = "Image";
|
||||
this.description = "Adjust the opacity of an image.";
|
||||
this.infoURL = "";
|
||||
this.inputType = "byteArray";
|
||||
this.outputType = "byteArray";
|
||||
this.presentType = "html";
|
||||
this.args = [
|
||||
{
|
||||
name: "Opacity (%)",
|
||||
type: "number",
|
||||
value: 100,
|
||||
min: 0,
|
||||
max: 100
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {byteArray} input
|
||||
* @param {Object[]} args
|
||||
* @returns {byteArray}
|
||||
*/
|
||||
async run(input, args) {
|
||||
const [opacity] = args;
|
||||
if (!isImage(input)) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
let image;
|
||||
try {
|
||||
image = await jimp.read(Buffer.from(input));
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error loading image. (${err})`);
|
||||
}
|
||||
try {
|
||||
if (ENVIRONMENT_IS_WORKER())
|
||||
self.sendStatusMessage("Changing image opacity...");
|
||||
image.opacity(opacity / 100);
|
||||
|
||||
const imageBuffer = await image.getBufferAsync(jimp.MIME_PNG);
|
||||
return [...imageBuffer];
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error changing image opacity. (${err})`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the image using HTML for web apps
|
||||
* @param {byteArray} data
|
||||
* @returns {html}
|
||||
*/
|
||||
present(data) {
|
||||
if (!data.length) return "";
|
||||
|
||||
const type = isImage(data);
|
||||
if (!type) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
return `<img src="data:${type};base64,${toBase64(data)}">`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default ImageOpacity;
|
||||
107
src/core/operations/IndexOfCoincidence.mjs
Normal file
107
src/core/operations/IndexOfCoincidence.mjs
Normal file
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* @author George O [georgeomnet+cyberchef@gmail.com]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import Utils from "../Utils";
|
||||
|
||||
/**
|
||||
* Index of Coincidence operation
|
||||
*/
|
||||
class IndexOfCoincidence extends Operation {
|
||||
|
||||
/**
|
||||
* IndexOfCoincidence constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Index of Coincidence";
|
||||
this.module = "Default";
|
||||
this.description = "Index of Coincidence (IC) is the probability of two randomly selected characters being the same. This can be used to determine whether text is readable or random, with English text having an IC of around 0.066. IC can therefore be a sound method to automate frequency analysis.";
|
||||
this.infoURL = "https://wikipedia.org/wiki/Index_of_coincidence";
|
||||
this.inputType = "string";
|
||||
this.outputType = "number";
|
||||
this.presentType = "html";
|
||||
this.args = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} input
|
||||
* @param {Object[]} args
|
||||
* @returns {number}
|
||||
*/
|
||||
run(input, args) {
|
||||
const text = input.toLowerCase().replace(/[^a-z]/g, ""),
|
||||
frequencies = new Array(26).fill(0),
|
||||
alphabet = Utils.expandAlphRange("a-z");
|
||||
let coincidence = 0.00,
|
||||
density = 0.00,
|
||||
result = 0.00,
|
||||
i;
|
||||
|
||||
for (i=0; i < alphabet.length; i++) {
|
||||
frequencies[i] = text.count(alphabet[i]);
|
||||
}
|
||||
|
||||
for (i=0; i < frequencies.length; i++) {
|
||||
coincidence += frequencies[i] * (frequencies[i] - 1);
|
||||
}
|
||||
|
||||
density = frequencies.sum();
|
||||
|
||||
// Ensure that we don't divide by 0
|
||||
if (density < 2) density = 2;
|
||||
|
||||
result = coincidence / (density * (density - 1));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the IC as a scale bar for web apps.
|
||||
*
|
||||
* @param {number} ic
|
||||
* @returns {html}
|
||||
*/
|
||||
present(ic) {
|
||||
return `Index of Coincidence: ${ic}
|
||||
Normalized: ${ic * 26}
|
||||
<br><canvas id='chart-area'></canvas><br>
|
||||
- 0 represents complete randomness (all characters are unique), whereas 1 represents no randomness (all characters are identical).
|
||||
- English text generally has an IC of between 0.67 to 0.78.
|
||||
- 'Random' text is determined by the probability that each letter occurs the same number of times as another.
|
||||
|
||||
The graph shows the IC of the input data. A low IC generally means that the text is random, compressed or encrypted.
|
||||
|
||||
<script type='application/javascript'>
|
||||
var canvas = document.getElementById("chart-area"),
|
||||
parentRect = canvas.parentNode.getBoundingClientRect(),
|
||||
ic = ${ic};
|
||||
|
||||
canvas.width = parentRect.width * 0.95;
|
||||
canvas.height = parentRect.height * 0.25;
|
||||
|
||||
ic = ic > 0.25 ? 0.25 : ic;
|
||||
|
||||
CanvasComponents.drawScaleBar(canvas, ic, 0.25, [
|
||||
{
|
||||
label: "English text",
|
||||
min: 0.05,
|
||||
max: 0.08
|
||||
},
|
||||
{
|
||||
label: "> 0.25",
|
||||
min: 0.24,
|
||||
max: 0.25
|
||||
}
|
||||
]);
|
||||
</script>
|
||||
`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default IndexOfCoincidence;
|
||||
79
src/core/operations/InvertImage.mjs
Normal file
79
src/core/operations/InvertImage.mjs
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* @author j433866 [j433866@gmail.com]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import { isImage } from "../lib/FileType";
|
||||
import { toBase64 } from "../lib/Base64";
|
||||
import jimp from "jimp";
|
||||
|
||||
/**
|
||||
* Invert Image operation
|
||||
*/
|
||||
class InvertImage extends Operation {
|
||||
|
||||
/**
|
||||
* InvertImage constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Invert Image";
|
||||
this.module = "Image";
|
||||
this.description = "Invert the colours of an image.";
|
||||
this.infoURL = "";
|
||||
this.inputType = "byteArray";
|
||||
this.outputType = "byteArray";
|
||||
this.presentType = "html";
|
||||
this.args = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {byteArray} input
|
||||
* @param {Object[]} args
|
||||
* @returns {byteArray}
|
||||
*/
|
||||
async run(input, args) {
|
||||
if (!isImage(input)) {
|
||||
throw new OperationError("Invalid input file format.");
|
||||
}
|
||||
|
||||
let image;
|
||||
try {
|
||||
image = await jimp.read(Buffer.from(input));
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error loading image. (${err})`);
|
||||
}
|
||||
try {
|
||||
if (ENVIRONMENT_IS_WORKER())
|
||||
self.sendStatusMessage("Inverting image...");
|
||||
image.invert();
|
||||
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
|
||||
return [...imageBuffer];
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error inverting image. (${err})`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the inverted image using HTML for web apps
|
||||
* @param {byteArray} data
|
||||
* @returns {html}
|
||||
*/
|
||||
present(data) {
|
||||
if (!data.length) return "";
|
||||
|
||||
const type = isImage(data);
|
||||
if (!type) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
return `<img src="data:${type};base64,${toBase64(data)}">`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default InvertImage;
|
||||
@@ -51,6 +51,10 @@ class JSONToCSV extends Operation {
|
||||
this.rowDelim = rowDelim;
|
||||
const self = this;
|
||||
|
||||
if (!(input instanceof Array)) {
|
||||
input = [input];
|
||||
}
|
||||
|
||||
try {
|
||||
// If the JSON is an array of arrays, this is easy
|
||||
if (input[0] instanceof Array) {
|
||||
@@ -89,6 +93,8 @@ class JSONToCSV extends Operation {
|
||||
* @returns {string}
|
||||
*/
|
||||
escapeCellContents(data) {
|
||||
if (typeof data === "number") data = data.toString();
|
||||
|
||||
// Double quotes should be doubled up
|
||||
data = data.replace(/"/g, '""');
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ class JavaScriptParser extends Operation {
|
||||
this.name = "JavaScript Parser";
|
||||
this.module = "Code";
|
||||
this.description = "Returns an Abstract Syntax Tree for valid JavaScript code.";
|
||||
this.infoURL = "https://en.wikipedia.org/wiki/Abstract_syntax_tree";
|
||||
this.infoURL = "https://wikipedia.org/wiki/Abstract_syntax_tree";
|
||||
this.inputType = "string";
|
||||
this.outputType = "string";
|
||||
this.args = [
|
||||
|
||||
305
src/core/operations/MultipleBombe.mjs
Normal file
305
src/core/operations/MultipleBombe.mjs
Normal file
@@ -0,0 +1,305 @@
|
||||
/**
|
||||
* Emulation of the Bombe machine.
|
||||
* This version carries out multiple Bombe runs to handle unknown rotor configurations.
|
||||
*
|
||||
* @author s2224834
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import {BombeMachine} from "../lib/Bombe";
|
||||
import {ROTORS, ROTORS_FOURTH, REFLECTORS, Reflector} from "../lib/Enigma";
|
||||
|
||||
/**
|
||||
* Convenience method for flattening the preset ROTORS object into a newline-separated string.
|
||||
* @param {Object[]} - Preset rotors object
|
||||
* @param {number} s - Start index
|
||||
* @param {number} n - End index
|
||||
* @returns {string}
|
||||
*/
|
||||
function rotorsFormat(rotors, s, n) {
|
||||
const res = [];
|
||||
for (const i of rotors.slice(s, n)) {
|
||||
res.push(i.value);
|
||||
}
|
||||
return res.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Combinatorics choose function
|
||||
* @param {number} n
|
||||
* @param {number} k
|
||||
* @returns number
|
||||
*/
|
||||
function choose(n, k) {
|
||||
let res = 1;
|
||||
for (let i=1; i<=k; i++) {
|
||||
res *= (n + 1 - i) / i;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bombe operation
|
||||
*/
|
||||
class MultipleBombe extends Operation {
|
||||
/**
|
||||
* Bombe constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Multiple Bombe";
|
||||
this.module = "Default";
|
||||
this.description = "Emulation of the Bombe machine used to attack Enigma. This version carries out multiple Bombe runs to handle unknown rotor configurations.<br><br>You should test your menu on the single Bombe operation before running it here. See the description of the Bombe operation for instructions on choosing a crib.<br><br>More detailed descriptions of the Enigma, Typex and Bombe operations <a href='https://github.com/gchq/CyberChef/wiki/Enigma,-the-Bombe,-and-Typex'>can be found here</a>.";
|
||||
this.infoURL = "https://wikipedia.org/wiki/Bombe";
|
||||
this.inputType = "string";
|
||||
this.outputType = "JSON";
|
||||
this.presentType = "html";
|
||||
this.args = [
|
||||
{
|
||||
"name": "Standard Enigmas",
|
||||
"type": "populateMultiOption",
|
||||
"value": [
|
||||
{
|
||||
name: "German Service Enigma (First - 3 rotor)",
|
||||
value: [
|
||||
rotorsFormat(ROTORS, 0, 5),
|
||||
"",
|
||||
rotorsFormat(REFLECTORS, 0, 1)
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "German Service Enigma (Second - 3 rotor)",
|
||||
value: [
|
||||
rotorsFormat(ROTORS, 0, 8),
|
||||
"",
|
||||
rotorsFormat(REFLECTORS, 0, 2)
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "German Service Enigma (Third - 4 rotor)",
|
||||
value: [
|
||||
rotorsFormat(ROTORS, 0, 8),
|
||||
rotorsFormat(ROTORS_FOURTH, 1, 2),
|
||||
rotorsFormat(REFLECTORS, 2, 3)
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "German Service Enigma (Fourth - 4 rotor)",
|
||||
value: [
|
||||
rotorsFormat(ROTORS, 0, 8),
|
||||
rotorsFormat(ROTORS_FOURTH, 1, 3),
|
||||
rotorsFormat(REFLECTORS, 2, 4)
|
||||
]
|
||||
},
|
||||
{
|
||||
name: "User defined",
|
||||
value: ["", "", ""]
|
||||
},
|
||||
],
|
||||
"target": [1, 2, 3]
|
||||
},
|
||||
{
|
||||
name: "Main rotors",
|
||||
type: "text",
|
||||
value: ""
|
||||
},
|
||||
{
|
||||
name: "4th rotor",
|
||||
type: "text",
|
||||
value: ""
|
||||
},
|
||||
{
|
||||
name: "Reflectors",
|
||||
type: "text",
|
||||
value: ""
|
||||
},
|
||||
{
|
||||
name: "Crib",
|
||||
type: "string",
|
||||
value: ""
|
||||
},
|
||||
{
|
||||
name: "Crib offset",
|
||||
type: "number",
|
||||
value: 0
|
||||
},
|
||||
{
|
||||
name: "Use checking machine",
|
||||
type: "boolean",
|
||||
value: true
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Format and send a status update message.
|
||||
* @param {number} nLoops - Number of loops in the menu
|
||||
* @param {number} nStops - How many stops so far
|
||||
* @param {number} progress - Progress (as a float in the range 0..1)
|
||||
*/
|
||||
updateStatus(nLoops, nStops, progress, start) {
|
||||
const elapsed = new Date().getTime() - start;
|
||||
const remaining = (elapsed / progress) * (1 - progress) / 1000;
|
||||
const hours = Math.floor(remaining / 3600);
|
||||
const minutes = `0${Math.floor((remaining % 3600) / 60)}`.slice(-2);
|
||||
const seconds = `0${Math.floor(remaining % 60)}`.slice(-2);
|
||||
const msg = `Bombe run with ${nLoops} loop${nLoops === 1 ? "" : "s"} in menu (2+ desirable): ${nStops} stops, ${Math.floor(100 * progress)}% done, ${hours}:${minutes}:${seconds} remaining`;
|
||||
self.sendStatusMessage(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Early rotor description string validation.
|
||||
* Drops stepping information.
|
||||
* @param {string} rstr - The rotor description string
|
||||
* @returns {string} - Rotor description with stepping stripped, if any
|
||||
*/
|
||||
validateRotor(rstr) {
|
||||
// The Bombe doesn't take stepping into account so we'll just ignore it here
|
||||
if (rstr.includes("<")) {
|
||||
rstr = rstr.split("<", 2)[0];
|
||||
}
|
||||
// Duplicate the validation of the rotor strings here, otherwise you might get an error
|
||||
// thrown halfway into a big Bombe run
|
||||
if (!/^[A-Z]{26}$/.test(rstr)) {
|
||||
throw new OperationError("Rotor wiring must be 26 unique uppercase letters");
|
||||
}
|
||||
if (new Set(rstr).size !== 26) {
|
||||
throw new OperationError("Rotor wiring must be 26 unique uppercase letters");
|
||||
}
|
||||
return rstr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} input
|
||||
* @param {Object[]} args
|
||||
* @returns {string}
|
||||
*/
|
||||
run(input, args) {
|
||||
const mainRotorsStr = args[1];
|
||||
const fourthRotorsStr = args[2];
|
||||
const reflectorsStr = args[3];
|
||||
let crib = args[4];
|
||||
const offset = args[5];
|
||||
const check = args[6];
|
||||
const rotors = [];
|
||||
const fourthRotors = [];
|
||||
const reflectors = [];
|
||||
for (let rstr of mainRotorsStr.split("\n")) {
|
||||
rstr = this.validateRotor(rstr);
|
||||
rotors.push(rstr);
|
||||
}
|
||||
if (rotors.length < 3) {
|
||||
throw new OperationError("A minimum of three rotors must be supplied");
|
||||
}
|
||||
if (fourthRotorsStr !== "") {
|
||||
for (let rstr of fourthRotorsStr.split("\n")) {
|
||||
rstr = this.validateRotor(rstr);
|
||||
fourthRotors.push(rstr);
|
||||
}
|
||||
}
|
||||
if (fourthRotors.length === 0) {
|
||||
fourthRotors.push("");
|
||||
}
|
||||
for (const rstr of reflectorsStr.split("\n")) {
|
||||
const reflector = new Reflector(rstr);
|
||||
reflectors.push(reflector);
|
||||
}
|
||||
if (reflectors.length === 0) {
|
||||
throw new OperationError("A minimum of one reflector must be supplied");
|
||||
}
|
||||
if (crib.length === 0) {
|
||||
throw new OperationError("Crib cannot be empty");
|
||||
}
|
||||
if (offset < 0) {
|
||||
throw new OperationError("Offset cannot be negative");
|
||||
}
|
||||
// For symmetry with the Enigma op, for the input we'll just remove all invalid characters
|
||||
input = input.replace(/[^A-Za-z]/g, "").toUpperCase();
|
||||
crib = crib.replace(/[^A-Za-z]/g, "").toUpperCase();
|
||||
const ciphertext = input.slice(offset);
|
||||
let update;
|
||||
if (ENVIRONMENT_IS_WORKER()) {
|
||||
update = this.updateStatus;
|
||||
} else {
|
||||
update = undefined;
|
||||
}
|
||||
let bombe = undefined;
|
||||
const output = {bombeRuns: []};
|
||||
// I could use a proper combinatorics algorithm here... but it would be more code to
|
||||
// write one, and we don't seem to have one in our existing libraries, so massively nested
|
||||
// for loop it is
|
||||
const totalRuns = choose(rotors.length, 3) * 6 * fourthRotors.length * reflectors.length;
|
||||
let nRuns = 0;
|
||||
let nStops = 0;
|
||||
const start = new Date().getTime();
|
||||
for (const rotor1 of rotors) {
|
||||
for (const rotor2 of rotors) {
|
||||
if (rotor2 === rotor1) {
|
||||
continue;
|
||||
}
|
||||
for (const rotor3 of rotors) {
|
||||
if (rotor3 === rotor2 || rotor3 === rotor1) {
|
||||
continue;
|
||||
}
|
||||
for (const rotor4 of fourthRotors) {
|
||||
for (const reflector of reflectors) {
|
||||
nRuns++;
|
||||
const runRotors = [rotor1, rotor2, rotor3];
|
||||
if (rotor4 !== "") {
|
||||
runRotors.push(rotor4);
|
||||
}
|
||||
if (bombe === undefined) {
|
||||
bombe = new BombeMachine(runRotors, reflector, ciphertext, crib, check);
|
||||
output.nLoops = bombe.nLoops;
|
||||
} else {
|
||||
bombe.changeRotors(runRotors, reflector);
|
||||
}
|
||||
const result = bombe.run();
|
||||
nStops += result.length;
|
||||
if (update !== undefined) {
|
||||
update(bombe.nLoops, nStops, nRuns / totalRuns, start);
|
||||
}
|
||||
if (result.length > 0) {
|
||||
output.bombeRuns.push({
|
||||
rotors: runRotors,
|
||||
reflector: reflector.pairs,
|
||||
result: result
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Displays the MultiBombe results in an HTML table
|
||||
*
|
||||
* @param {Object} output
|
||||
* @param {number} output.nLoops
|
||||
* @param {Array[]} output.result
|
||||
* @returns {html}
|
||||
*/
|
||||
present(output) {
|
||||
let html = `Bombe run on menu with ${output.nLoops} loop${output.nLoops === 1 ? "" : "s"} (2+ desirable). Note: Rotors and rotor positions are listed left to right, ignore stepping and the ring setting, and positions start at the beginning of the crib. Some plugboard settings are determined. A decryption preview starting at the beginning of the crib and ignoring stepping is also provided.\n`;
|
||||
|
||||
for (const run of output.bombeRuns) {
|
||||
html += `\nRotors: ${run.rotors.slice().reverse().join(", ")}\nReflector: ${run.reflector}\n`;
|
||||
html += "<table class='table table-hover table-sm table-bordered table-nonfluid'><tr><th>Rotor stops</th> <th>Partial plugboard</th> <th>Decryption preview</th></tr>\n";
|
||||
for (const [setting, stecker, decrypt] of run.result) {
|
||||
html += `<tr><td>${setting}</td> <td>${stecker}</td> <td>${decrypt}</td></tr>\n`;
|
||||
}
|
||||
html += "</table>\n";
|
||||
}
|
||||
return html;
|
||||
}
|
||||
}
|
||||
|
||||
export default MultipleBombe;
|
||||
70
src/core/operations/NormaliseImage.mjs
Normal file
70
src/core/operations/NormaliseImage.mjs
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* @author j433866 [j433866@gmail.com]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import { isImage } from "../lib/FileType";
|
||||
import { toBase64 } from "../lib/Base64";
|
||||
import jimp from "jimp";
|
||||
|
||||
/**
|
||||
* Normalise Image operation
|
||||
*/
|
||||
class NormaliseImage extends Operation {
|
||||
|
||||
/**
|
||||
* NormaliseImage constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Normalise Image";
|
||||
this.module = "Image";
|
||||
this.description = "Normalise the image colours.";
|
||||
this.infoURL = "";
|
||||
this.inputType = "byteArray";
|
||||
this.outputType = "byteArray";
|
||||
this.presentType= "html";
|
||||
this.args = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {byteArray} input
|
||||
* @param {Object[]} args
|
||||
* @returns {byteArray}
|
||||
*/
|
||||
async run(input, args) {
|
||||
if (!isImage(input)) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
const image = await jimp.read(Buffer.from(input));
|
||||
|
||||
image.normalize();
|
||||
|
||||
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
|
||||
return [...imageBuffer];
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the normalised image using HTML for web apps
|
||||
* @param {byteArray} data
|
||||
* @returns {html}
|
||||
*/
|
||||
present(data) {
|
||||
if (!data.length) return "";
|
||||
|
||||
const type = isImage(data);
|
||||
if (!type) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
return `<img src="data:${type};base64,${toBase64(data)}">`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default NormaliseImage;
|
||||
@@ -21,7 +21,7 @@ class PEMToHex extends Operation {
|
||||
this.name = "PEM to Hex";
|
||||
this.module = "PublicKey";
|
||||
this.description = "Converts PEM (Privacy Enhanced Mail) format to a hexadecimal DER (Distinguished Encoding Rules) string.";
|
||||
this.infoURL = "https://en.wikipedia.org/wiki/X.690#DER_encoding";
|
||||
this.infoURL = "https://wikipedia.org/wiki/X.690#DER_encoding";
|
||||
this.inputType = "string";
|
||||
this.outputType = "string";
|
||||
this.args = [];
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import Magic from "../lib/Magic";
|
||||
import { isImage } from "../lib/FileType";
|
||||
import jsqr from "jsqr";
|
||||
import jimp from "jimp";
|
||||
|
||||
@@ -42,64 +42,61 @@ class ParseQRCode extends Operation {
|
||||
* @returns {string}
|
||||
*/
|
||||
async run(input, args) {
|
||||
const type = Magic.magicFileType(input);
|
||||
const [normalise] = args;
|
||||
|
||||
// Make sure that the input is an image
|
||||
if (type && type.mime.indexOf("image") === 0) {
|
||||
let image = input;
|
||||
if (!isImage(input)) throw new OperationError("Invalid file type.");
|
||||
|
||||
if (normalise) {
|
||||
// Process the image to be easier to read by jsqr
|
||||
// Disables the alpha channel
|
||||
// Sets the image default background to white
|
||||
// Normalises the image colours
|
||||
// Makes the image greyscale
|
||||
// Converts image to a JPEG
|
||||
image = await new Promise((resolve, reject) => {
|
||||
jimp.read(Buffer.from(input))
|
||||
.then(image => {
|
||||
image
|
||||
.rgba(false)
|
||||
.background(0xFFFFFFFF)
|
||||
.normalize()
|
||||
.greyscale()
|
||||
.getBuffer(jimp.MIME_JPEG, (error, result) => {
|
||||
resolve(result);
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
reject(new OperationError("Error reading the image file."));
|
||||
});
|
||||
});
|
||||
}
|
||||
let image = input;
|
||||
|
||||
if (image instanceof OperationError) {
|
||||
throw image;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
jimp.read(Buffer.from(image))
|
||||
if (normalise) {
|
||||
// Process the image to be easier to read by jsqr
|
||||
// Disables the alpha channel
|
||||
// Sets the image default background to white
|
||||
// Normalises the image colours
|
||||
// Makes the image greyscale
|
||||
// Converts image to a JPEG
|
||||
image = await new Promise((resolve, reject) => {
|
||||
jimp.read(Buffer.from(input))
|
||||
.then(image => {
|
||||
if (image.bitmap != null) {
|
||||
const qrData = jsqr(image.bitmap.data, image.getWidth(), image.getHeight());
|
||||
if (qrData != null) {
|
||||
resolve(qrData.data);
|
||||
} else {
|
||||
reject(new OperationError("Couldn't read a QR code from the image."));
|
||||
}
|
||||
} else {
|
||||
reject(new OperationError("Error reading the image file."));
|
||||
}
|
||||
image
|
||||
.rgba(false)
|
||||
.background(0xFFFFFFFF)
|
||||
.normalize()
|
||||
.greyscale()
|
||||
.getBuffer(jimp.MIME_JPEG, (error, result) => {
|
||||
resolve(result);
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
reject(new OperationError("Error reading the image file."));
|
||||
});
|
||||
});
|
||||
} else {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
if (image instanceof OperationError) {
|
||||
throw image;
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
jimp.read(Buffer.from(image))
|
||||
.then(image => {
|
||||
if (image.bitmap != null) {
|
||||
const qrData = jsqr(image.bitmap.data, image.getWidth(), image.getHeight());
|
||||
if (qrData != null) {
|
||||
resolve(qrData.data);
|
||||
} else {
|
||||
reject(new OperationError("Couldn't read a QR code from the image."));
|
||||
}
|
||||
} else {
|
||||
reject(new OperationError("Error reading the image file."));
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
reject(new OperationError("Error reading the image file."));
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { fromHex } from "../lib/Hex";
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import Utils from "../Utils";
|
||||
import Magic from "../lib/Magic";
|
||||
import { isType, detectFileType } from "../lib/FileType";
|
||||
|
||||
/**
|
||||
* PlayMedia operation
|
||||
@@ -66,8 +66,7 @@ class PlayMedia extends Operation {
|
||||
|
||||
|
||||
// Determine file type
|
||||
const type = Magic.magicFileType(input);
|
||||
if (!(type && /^audio|video/.test(type.mime))) {
|
||||
if (!isType(/^(audio|video)/, input)) {
|
||||
throw new OperationError("Invalid or unrecognised file type");
|
||||
}
|
||||
|
||||
@@ -84,15 +83,15 @@ class PlayMedia extends Operation {
|
||||
async present(data) {
|
||||
if (!data.length) return "";
|
||||
|
||||
const type = Magic.magicFileType(data);
|
||||
const matches = /^audio|video/.exec(type.mime);
|
||||
const types = detectFileType(data);
|
||||
const matches = /^audio|video/.exec(types[0].mime);
|
||||
if (!matches) {
|
||||
throw new OperationError("Invalid file type");
|
||||
}
|
||||
const dataURI = `data:${type.mime};base64,${toBase64(data)}`;
|
||||
const dataURI = `data:${types[0].mime};base64,${toBase64(data)}`;
|
||||
const element = matches[0];
|
||||
|
||||
let html = `<${element} src='${dataURI}' type='${type.mime}' controls>`;
|
||||
let html = `<${element} src='${dataURI}' type='${types[0].mime}' controls>`;
|
||||
html += "<p>Unsupported media type.</p>";
|
||||
html += `</${element}>`;
|
||||
return html;
|
||||
|
||||
46
src/core/operations/ProtobufDecode.mjs
Normal file
46
src/core/operations/ProtobufDecode.mjs
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* @author GCHQ Contributor [3]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import Protobuf from "../lib/Protobuf";
|
||||
|
||||
/**
|
||||
* Protobuf Decode operation
|
||||
*/
|
||||
class ProtobufDecode extends Operation {
|
||||
|
||||
/**
|
||||
* ProtobufDecode constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Protobuf Decode";
|
||||
this.module = "Default";
|
||||
this.description = "Decodes any Protobuf encoded data to a JSON representation of the data using the field number as the field key.";
|
||||
this.infoURL = "https://wikipedia.org/wiki/Protocol_Buffers";
|
||||
this.inputType = "byteArray";
|
||||
this.outputType = "JSON";
|
||||
this.args = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {byteArray} input
|
||||
* @param {Object[]} args
|
||||
* @returns {JSON}
|
||||
*/
|
||||
run(input, args) {
|
||||
try {
|
||||
return Protobuf.decode(input);
|
||||
} catch (err) {
|
||||
throw new OperationError(err);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default ProtobufDecode;
|
||||
@@ -230,6 +230,7 @@ function regexHighlight (input, regex, displayTotal) {
|
||||
title = "",
|
||||
hl = 1,
|
||||
total = 0;
|
||||
const captureGroups = [];
|
||||
|
||||
output = input.replace(regex, (match, ...args) => {
|
||||
args.pop(); // Throw away full string
|
||||
@@ -247,9 +248,15 @@ function regexHighlight (input, regex, displayTotal) {
|
||||
// Switch highlight
|
||||
hl = hl === 1 ? 2 : 1;
|
||||
|
||||
total++;
|
||||
// Store highlighted match and replace with a placeholder
|
||||
captureGroups.push(`<span class='hl${hl}' title='${title}'>${Utils.escapeHtml(match)}</span>`);
|
||||
return `[cc_capture_group_${total++}]`;
|
||||
});
|
||||
|
||||
return `<span class='hl${hl}' title='${title}'>${Utils.escapeHtml(match)}</span>`;
|
||||
// Safely escape all remaining text, then replace placeholders
|
||||
output = Utils.escapeHtml(output);
|
||||
output = output.replace(/\[cc_capture_group_(\d+)\]/g, (_, i) => {
|
||||
return captureGroups[i];
|
||||
});
|
||||
|
||||
if (displayTotal)
|
||||
|
||||
@@ -9,7 +9,7 @@ import { fromHex } from "../lib/Hex";
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import Utils from "../Utils";
|
||||
import Magic from "../lib/Magic";
|
||||
import {isImage} from "../lib/FileType";
|
||||
|
||||
/**
|
||||
* Render Image operation
|
||||
@@ -72,8 +72,7 @@ class RenderImage extends Operation {
|
||||
}
|
||||
|
||||
// Determine file type
|
||||
const type = Magic.magicFileType(input);
|
||||
if (!(type && type.mime.indexOf("image") === 0)) {
|
||||
if (!isImage(input)) {
|
||||
throw new OperationError("Invalid file type");
|
||||
}
|
||||
|
||||
@@ -92,9 +91,9 @@ class RenderImage extends Operation {
|
||||
let dataURI = "data:";
|
||||
|
||||
// Determine file type
|
||||
const type = Magic.magicFileType(data);
|
||||
if (type && type.mime.indexOf("image") === 0) {
|
||||
dataURI += type.mime + ";";
|
||||
const mime = isImage(data);
|
||||
if (mime) {
|
||||
dataURI += mime + ";";
|
||||
} else {
|
||||
throw new OperationError("Invalid file type");
|
||||
}
|
||||
|
||||
138
src/core/operations/ResizeImage.mjs
Normal file
138
src/core/operations/ResizeImage.mjs
Normal file
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* @author j433866 [j433866@gmail.com]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import { isImage } from "../lib/FileType";
|
||||
import { toBase64 } from "../lib/Base64.mjs";
|
||||
import jimp from "jimp";
|
||||
|
||||
/**
|
||||
* Resize Image operation
|
||||
*/
|
||||
class ResizeImage extends Operation {
|
||||
|
||||
/**
|
||||
* ResizeImage constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Resize Image";
|
||||
this.module = "Image";
|
||||
this.description = "Resizes an image to the specified width and height values.";
|
||||
this.infoURL = "https://wikipedia.org/wiki/Image_scaling";
|
||||
this.inputType = "byteArray";
|
||||
this.outputType = "byteArray";
|
||||
this.presentType = "html";
|
||||
this.args = [
|
||||
{
|
||||
name: "Width",
|
||||
type: "number",
|
||||
value: 100,
|
||||
min: 1
|
||||
},
|
||||
{
|
||||
name: "Height",
|
||||
type: "number",
|
||||
value: 100,
|
||||
min: 1
|
||||
},
|
||||
{
|
||||
name: "Unit type",
|
||||
type: "option",
|
||||
value: ["Pixels", "Percent"]
|
||||
},
|
||||
{
|
||||
name: "Maintain aspect ratio",
|
||||
type: "boolean",
|
||||
value: false
|
||||
},
|
||||
{
|
||||
name: "Resizing algorithm",
|
||||
type: "option",
|
||||
value: [
|
||||
"Nearest Neighbour",
|
||||
"Bilinear",
|
||||
"Bicubic",
|
||||
"Hermite",
|
||||
"Bezier"
|
||||
],
|
||||
defaultIndex: 1
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {byteArray} input
|
||||
* @param {Object[]} args
|
||||
* @returns {byteArray}
|
||||
*/
|
||||
async run(input, args) {
|
||||
let width = args[0],
|
||||
height = args[1];
|
||||
const unit = args[2],
|
||||
aspect = args[3],
|
||||
resizeAlg = args[4];
|
||||
|
||||
const resizeMap = {
|
||||
"Nearest Neighbour": jimp.RESIZE_NEAREST_NEIGHBOR,
|
||||
"Bilinear": jimp.RESIZE_BILINEAR,
|
||||
"Bicubic": jimp.RESIZE_BICUBIC,
|
||||
"Hermite": jimp.RESIZE_HERMITE,
|
||||
"Bezier": jimp.RESIZE_BEZIER
|
||||
};
|
||||
|
||||
if (!isImage(input)) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
let image;
|
||||
try {
|
||||
image = await jimp.read(Buffer.from(input));
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error loading image. (${err})`);
|
||||
}
|
||||
try {
|
||||
if (unit === "Percent") {
|
||||
width = image.getWidth() * (width / 100);
|
||||
height = image.getHeight() * (height / 100);
|
||||
}
|
||||
|
||||
if (ENVIRONMENT_IS_WORKER())
|
||||
self.sendStatusMessage("Resizing image...");
|
||||
if (aspect) {
|
||||
image.scaleToFit(width, height, resizeMap[resizeAlg]);
|
||||
} else {
|
||||
image.resize(width, height, resizeMap[resizeAlg]);
|
||||
}
|
||||
|
||||
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
|
||||
return [...imageBuffer];
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error resizing image. (${err})`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the resized image using HTML for web apps
|
||||
* @param {byteArray} data
|
||||
* @returns {html}
|
||||
*/
|
||||
present(data) {
|
||||
if (!data.length) return "";
|
||||
|
||||
const type = isImage(data);
|
||||
if (!type) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
return `<img src="data:${type};base64,${toBase64(data)}">`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default ResizeImage;
|
||||
87
src/core/operations/RotateImage.mjs
Normal file
87
src/core/operations/RotateImage.mjs
Normal file
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* @author j433866 [j433866@gmail.com]
|
||||
* @copyright Crown Copyright 2018
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import { isImage } from "../lib/FileType";
|
||||
import { toBase64 } from "../lib/Base64";
|
||||
import jimp from "jimp";
|
||||
|
||||
/**
|
||||
* Rotate Image operation
|
||||
*/
|
||||
class RotateImage extends Operation {
|
||||
|
||||
/**
|
||||
* RotateImage constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Rotate Image";
|
||||
this.module = "Image";
|
||||
this.description = "Rotates an image by the specified number of degrees.";
|
||||
this.infoURL = "";
|
||||
this.inputType = "byteArray";
|
||||
this.outputType = "byteArray";
|
||||
this.presentType = "html";
|
||||
this.args = [
|
||||
{
|
||||
name: "Rotation amount (degrees)",
|
||||
type: "number",
|
||||
value: 90
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {byteArray} input
|
||||
* @param {Object[]} args
|
||||
* @returns {byteArray}
|
||||
*/
|
||||
async run(input, args) {
|
||||
const [degrees] = args;
|
||||
|
||||
if (!isImage(input)) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
let image;
|
||||
try {
|
||||
image = await jimp.read(Buffer.from(input));
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error loading image. (${err})`);
|
||||
}
|
||||
try {
|
||||
if (ENVIRONMENT_IS_WORKER())
|
||||
self.sendStatusMessage("Rotating image...");
|
||||
image.rotate(degrees);
|
||||
const imageBuffer = await image.getBufferAsync(jimp.AUTO);
|
||||
return [...imageBuffer];
|
||||
} catch (err) {
|
||||
throw new OperationError(`Error rotating image. (${err})`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays the rotated image using HTML for web apps
|
||||
* @param {byteArray} data
|
||||
* @returns {html}
|
||||
*/
|
||||
present(data) {
|
||||
if (!data.length) return "";
|
||||
|
||||
const type = isImage(data);
|
||||
if (!type) {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
|
||||
return `<img src="data:${type};base64,${toBase64(data)}">`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default RotateImage;
|
||||
@@ -6,7 +6,8 @@
|
||||
|
||||
import Operation from "../Operation";
|
||||
import Utils from "../Utils";
|
||||
import Magic from "../lib/Magic";
|
||||
import {scanForFileTypes} from "../lib/FileType";
|
||||
import {FILE_SIGNATURES} from "../lib/FileSignatures";
|
||||
|
||||
/**
|
||||
* Scan for Embedded Files operation
|
||||
@@ -25,13 +26,13 @@ class ScanForEmbeddedFiles extends Operation {
|
||||
this.infoURL = "https://wikipedia.org/wiki/List_of_file_signatures";
|
||||
this.inputType = "ArrayBuffer";
|
||||
this.outputType = "string";
|
||||
this.args = [
|
||||
{
|
||||
"name": "Ignore common byte sequences",
|
||||
"type": "boolean",
|
||||
"value": true
|
||||
}
|
||||
];
|
||||
this.args = Object.keys(FILE_SIGNATURES).map(cat => {
|
||||
return {
|
||||
name: cat,
|
||||
type: "boolean",
|
||||
value: cat === "Miscellaneous" ? false : true
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -41,43 +42,33 @@ class ScanForEmbeddedFiles extends Operation {
|
||||
*/
|
||||
run(input, args) {
|
||||
let output = "Scanning data for 'magic bytes' which may indicate embedded files. The following results may be false positives and should not be treat as reliable. Any suffiently long file is likely to contain these magic bytes coincidentally.\n",
|
||||
type,
|
||||
numFound = 0,
|
||||
numCommonFound = 0;
|
||||
const ignoreCommon = args[0],
|
||||
commonExts = ["ico", "ttf", ""],
|
||||
numFound = 0;
|
||||
const categories = [],
|
||||
data = new Uint8Array(input);
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
type = Magic.magicFileType(data.slice(i));
|
||||
if (type) {
|
||||
if (ignoreCommon && commonExts.indexOf(type.ext) > -1) {
|
||||
numCommonFound++;
|
||||
continue;
|
||||
}
|
||||
numFound++;
|
||||
output += "\nOffset " + i + " (0x" + Utils.hex(i) + "):\n" +
|
||||
" File extension: " + type.ext + "\n" +
|
||||
" MIME type: " + type.mime + "\n";
|
||||
args.forEach((cat, i) => {
|
||||
if (cat) categories.push(Object.keys(FILE_SIGNATURES)[i]);
|
||||
});
|
||||
|
||||
if (type.desc && type.desc.length) {
|
||||
output += " Description: " + type.desc + "\n";
|
||||
const types = scanForFileTypes(data, categories);
|
||||
|
||||
if (types.length) {
|
||||
types.forEach(type => {
|
||||
numFound++;
|
||||
output += "\nOffset " + type.offset + " (0x" + Utils.hex(type.offset) + "):\n" +
|
||||
" File extension: " + type.fileDetails.extension + "\n" +
|
||||
" MIME type: " + type.fileDetails.mime + "\n";
|
||||
|
||||
if (type.fileDetails.description && type.fileDetails.description.length) {
|
||||
output += " Description: " + type.fileDetails.description + "\n";
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (numFound === 0) {
|
||||
output += "\nNo embedded files were found.";
|
||||
}
|
||||
|
||||
if (numCommonFound > 0) {
|
||||
output += "\n\n" + numCommonFound;
|
||||
output += numCommonFound === 1 ?
|
||||
" file type was detected that has a common byte sequence. This is likely to be a false positive." :
|
||||
" file types were detected that have common byte sequences. These are likely to be false positives.";
|
||||
output += " Run this operation with the 'Ignore common byte sequences' option unchecked to see details.";
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
|
||||
199
src/core/operations/ScatterChart.mjs
Normal file
199
src/core/operations/ScatterChart.mjs
Normal file
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* @author tlwr [toby@toby.codes]
|
||||
* @author Matt C [me@mitt.dev]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import * as d3temp from "d3";
|
||||
import * as nodomtemp from "nodom";
|
||||
import { getScatterValues, getScatterValuesWithColour, RECORD_DELIMITER_OPTIONS, COLOURS, FIELD_DELIMITER_OPTIONS } from "../lib/Charts";
|
||||
|
||||
import Operation from "../Operation";
|
||||
import Utils from "../Utils";
|
||||
|
||||
const d3 = d3temp.default ? d3temp.default : d3temp;
|
||||
const nodom = nodomtemp.default ? nodomtemp.default: nodomtemp;
|
||||
|
||||
/**
|
||||
* Scatter chart operation
|
||||
*/
|
||||
class ScatterChart extends Operation {
|
||||
|
||||
/**
|
||||
* ScatterChart constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Scatter chart";
|
||||
this.module = "Charts";
|
||||
this.description = "Plots two-variable data as single points on a graph.";
|
||||
this.infoURL = "https://wikipedia.org/wiki/Scatter_plot";
|
||||
this.inputType = "string";
|
||||
this.outputType = "html";
|
||||
this.args = [
|
||||
{
|
||||
name: "Record delimiter",
|
||||
type: "option",
|
||||
value: RECORD_DELIMITER_OPTIONS,
|
||||
},
|
||||
{
|
||||
name: "Field delimiter",
|
||||
type: "option",
|
||||
value: FIELD_DELIMITER_OPTIONS,
|
||||
},
|
||||
{
|
||||
name: "Use column headers as labels",
|
||||
type: "boolean",
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
name: "X label",
|
||||
type: "string",
|
||||
value: "",
|
||||
},
|
||||
{
|
||||
name: "Y label",
|
||||
type: "string",
|
||||
value: "",
|
||||
},
|
||||
{
|
||||
name: "Colour",
|
||||
type: "string",
|
||||
value: COLOURS.max,
|
||||
},
|
||||
{
|
||||
name: "Point radius",
|
||||
type: "number",
|
||||
value: 10,
|
||||
},
|
||||
{
|
||||
name: "Use colour from third column",
|
||||
type: "boolean",
|
||||
value: false,
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Scatter chart operation.
|
||||
*
|
||||
* @param {string} input
|
||||
* @param {Object[]} args
|
||||
* @returns {html}
|
||||
*/
|
||||
run(input, args) {
|
||||
const recordDelimiter = Utils.charRep(args[0]),
|
||||
fieldDelimiter = Utils.charRep(args[1]),
|
||||
columnHeadingsAreIncluded = args[2],
|
||||
fillColour = args[5],
|
||||
radius = args[6],
|
||||
colourInInput = args[7],
|
||||
dimension = 500;
|
||||
|
||||
let xLabel = args[3],
|
||||
yLabel = args[4];
|
||||
|
||||
const dataFunction = colourInInput ? getScatterValuesWithColour : getScatterValues;
|
||||
const { headings, values } = dataFunction(
|
||||
input,
|
||||
recordDelimiter,
|
||||
fieldDelimiter,
|
||||
columnHeadingsAreIncluded
|
||||
);
|
||||
|
||||
if (headings) {
|
||||
xLabel = headings.x;
|
||||
yLabel = headings.y;
|
||||
}
|
||||
|
||||
const document = new nodom.Document();
|
||||
let svg = document.createElement("svg");
|
||||
svg = d3.select(svg)
|
||||
.attr("width", "100%")
|
||||
.attr("height", "100%")
|
||||
.attr("viewBox", `0 0 ${dimension} ${dimension}`);
|
||||
|
||||
const margin = {
|
||||
top: 10,
|
||||
right: 0,
|
||||
bottom: 40,
|
||||
left: 30,
|
||||
},
|
||||
width = dimension - margin.left - margin.right,
|
||||
height = dimension - margin.top - margin.bottom,
|
||||
marginedSpace = svg.append("g")
|
||||
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
|
||||
|
||||
const xExtent = d3.extent(values, d => d[0]),
|
||||
xDelta = xExtent[1] - xExtent[0],
|
||||
yExtent = d3.extent(values, d => d[1]),
|
||||
yDelta = yExtent[1] - yExtent[0],
|
||||
xAxis = d3.scaleLinear()
|
||||
.domain([xExtent[0] - (0.1 * xDelta), xExtent[1] + (0.1 * xDelta)])
|
||||
.range([0, width]),
|
||||
yAxis = d3.scaleLinear()
|
||||
.domain([yExtent[0] - (0.1 * yDelta), yExtent[1] + (0.1 * yDelta)])
|
||||
.range([height, 0]);
|
||||
|
||||
marginedSpace.append("clipPath")
|
||||
.attr("id", "clip")
|
||||
.append("rect")
|
||||
.attr("width", width)
|
||||
.attr("height", height);
|
||||
|
||||
marginedSpace.append("g")
|
||||
.attr("class", "points")
|
||||
.attr("clip-path", "url(#clip)")
|
||||
.selectAll("circle")
|
||||
.data(values)
|
||||
.enter()
|
||||
.append("circle")
|
||||
.attr("cx", (d) => xAxis(d[0]))
|
||||
.attr("cy", (d) => yAxis(d[1]))
|
||||
.attr("r", d => radius)
|
||||
.attr("fill", d => {
|
||||
return colourInInput ? d[2] : fillColour;
|
||||
})
|
||||
.attr("stroke", "rgba(0, 0, 0, 0.5)")
|
||||
.attr("stroke-width", "0.5")
|
||||
.append("title")
|
||||
.text(d => {
|
||||
const x = d[0],
|
||||
y = d[1],
|
||||
tooltip = `X: ${x}\n
|
||||
Y: ${y}\n
|
||||
`.replace(/\s{2,}/g, "\n");
|
||||
return tooltip;
|
||||
});
|
||||
|
||||
marginedSpace.append("g")
|
||||
.attr("class", "axis axis--y")
|
||||
.call(d3.axisLeft(yAxis).tickSizeOuter(-width));
|
||||
|
||||
svg.append("text")
|
||||
.attr("transform", "rotate(-90)")
|
||||
.attr("y", -margin.left)
|
||||
.attr("x", -(height / 2))
|
||||
.attr("dy", "1em")
|
||||
.style("text-anchor", "middle")
|
||||
.text(yLabel);
|
||||
|
||||
marginedSpace.append("g")
|
||||
.attr("class", "axis axis--x")
|
||||
.attr("transform", "translate(0," + height + ")")
|
||||
.call(d3.axisBottom(xAxis).tickSizeOuter(-height));
|
||||
|
||||
svg.append("text")
|
||||
.attr("x", width / 2)
|
||||
.attr("y", dimension)
|
||||
.style("text-anchor", "middle")
|
||||
.text(xLabel);
|
||||
|
||||
return svg._groups[0][0].outerHTML;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default ScatterChart;
|
||||
227
src/core/operations/SeriesChart.mjs
Normal file
227
src/core/operations/SeriesChart.mjs
Normal file
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* @author tlwr [toby@toby.codes]
|
||||
* @author Matt C [me@mitt.dev]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import * as d3temp from "d3";
|
||||
import * as nodomtemp from "nodom";
|
||||
import { getSeriesValues, RECORD_DELIMITER_OPTIONS, FIELD_DELIMITER_OPTIONS } from "../lib/Charts";
|
||||
|
||||
import Operation from "../Operation";
|
||||
import Utils from "../Utils";
|
||||
|
||||
const d3 = d3temp.default ? d3temp.default : d3temp;
|
||||
const nodom = nodomtemp.default ? nodomtemp.default: nodomtemp;
|
||||
|
||||
/**
|
||||
* Series chart operation
|
||||
*/
|
||||
class SeriesChart extends Operation {
|
||||
|
||||
/**
|
||||
* SeriesChart constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Series chart";
|
||||
this.module = "Charts";
|
||||
this.description = "A time series graph is a line graph of repeated measurements taken over regular time intervals.";
|
||||
this.inputType = "string";
|
||||
this.outputType = "html";
|
||||
this.args = [
|
||||
{
|
||||
name: "Record delimiter",
|
||||
type: "option",
|
||||
value: RECORD_DELIMITER_OPTIONS,
|
||||
},
|
||||
{
|
||||
name: "Field delimiter",
|
||||
type: "option",
|
||||
value: FIELD_DELIMITER_OPTIONS,
|
||||
},
|
||||
{
|
||||
name: "X label",
|
||||
type: "string",
|
||||
value: "",
|
||||
},
|
||||
{
|
||||
name: "Point radius",
|
||||
type: "number",
|
||||
value: 1,
|
||||
},
|
||||
{
|
||||
name: "Series colours",
|
||||
type: "string",
|
||||
value: "mediumseagreen, dodgerblue, tomato",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Series chart operation.
|
||||
*
|
||||
* @param {string} input
|
||||
* @param {Object[]} args
|
||||
* @returns {html}
|
||||
*/
|
||||
run(input, args) {
|
||||
const recordDelimiter = Utils.charRep(args[0]),
|
||||
fieldDelimiter = Utils.charRep(args[1]),
|
||||
xLabel = args[2],
|
||||
pipRadius = args[3],
|
||||
seriesColours = args[4].split(","),
|
||||
svgWidth = 500,
|
||||
interSeriesPadding = 20,
|
||||
xAxisHeight = 50,
|
||||
seriesLabelWidth = 50,
|
||||
seriesHeight = 100,
|
||||
seriesWidth = svgWidth - seriesLabelWidth - interSeriesPadding;
|
||||
|
||||
const { xValues, series } = getSeriesValues(input, recordDelimiter, fieldDelimiter),
|
||||
allSeriesHeight = Object.keys(series).length * (interSeriesPadding + seriesHeight),
|
||||
svgHeight = allSeriesHeight + xAxisHeight + interSeriesPadding;
|
||||
|
||||
const document = new nodom.Document();
|
||||
let svg = document.createElement("svg");
|
||||
svg = d3.select(svg)
|
||||
.attr("width", "100%")
|
||||
.attr("height", "100%")
|
||||
.attr("viewBox", `0 0 ${svgWidth} ${svgHeight}`);
|
||||
|
||||
const xAxis = d3.scalePoint()
|
||||
.domain(xValues)
|
||||
.range([0, seriesWidth]);
|
||||
|
||||
svg.append("g")
|
||||
.attr("class", "axis axis--x")
|
||||
.attr("transform", `translate(${seriesLabelWidth}, ${xAxisHeight})`)
|
||||
.call(
|
||||
d3.axisTop(xAxis).tickValues(xValues.filter((x, i) => {
|
||||
return [0, Math.round(xValues.length / 2), xValues.length -1].indexOf(i) >= 0;
|
||||
}))
|
||||
);
|
||||
|
||||
svg.append("text")
|
||||
.attr("x", svgWidth / 2)
|
||||
.attr("y", xAxisHeight / 2)
|
||||
.style("text-anchor", "middle")
|
||||
.text(xLabel);
|
||||
|
||||
const tooltipText = {},
|
||||
tooltipAreaWidth = seriesWidth / xValues.length;
|
||||
|
||||
xValues.forEach(x => {
|
||||
const tooltip = [];
|
||||
|
||||
series.forEach(serie => {
|
||||
const y = serie.data[x];
|
||||
if (typeof y === "undefined") return;
|
||||
|
||||
tooltip.push(`${serie.name}: ${y}`);
|
||||
});
|
||||
|
||||
tooltipText[x] = tooltip.join("\n");
|
||||
});
|
||||
|
||||
const chartArea = svg.append("g")
|
||||
.attr("transform", `translate(${seriesLabelWidth}, ${xAxisHeight})`);
|
||||
|
||||
chartArea
|
||||
.append("g")
|
||||
.selectAll("rect")
|
||||
.data(xValues)
|
||||
.enter()
|
||||
.append("rect")
|
||||
.attr("x", x => {
|
||||
return xAxis(x) - (tooltipAreaWidth / 2);
|
||||
})
|
||||
.attr("y", 0)
|
||||
.attr("width", tooltipAreaWidth)
|
||||
.attr("height", allSeriesHeight)
|
||||
.attr("stroke", "none")
|
||||
.attr("fill", "transparent")
|
||||
.append("title")
|
||||
.text(x => {
|
||||
return `${x}\n
|
||||
--\n
|
||||
${tooltipText[x]}\n
|
||||
`.replace(/\s{2,}/g, "\n");
|
||||
});
|
||||
|
||||
const yAxesArea = svg.append("g")
|
||||
.attr("transform", `translate(0, ${xAxisHeight})`);
|
||||
|
||||
series.forEach((serie, seriesIndex) => {
|
||||
const yExtent = d3.extent(Object.values(serie.data)),
|
||||
yAxis = d3.scaleLinear()
|
||||
.domain(yExtent)
|
||||
.range([seriesHeight, 0]);
|
||||
|
||||
const seriesGroup = chartArea
|
||||
.append("g")
|
||||
.attr("transform", `translate(0, ${seriesHeight * seriesIndex + interSeriesPadding * (seriesIndex + 1)})`);
|
||||
|
||||
let path = "";
|
||||
xValues.forEach((x, xIndex) => {
|
||||
let nextX = xValues[xIndex + 1],
|
||||
y = serie.data[x],
|
||||
nextY= serie.data[nextX];
|
||||
|
||||
if (typeof y === "undefined" || typeof nextY === "undefined") return;
|
||||
|
||||
x = xAxis(x); nextX = xAxis(nextX);
|
||||
y = yAxis(y); nextY = yAxis(nextY);
|
||||
|
||||
path += `M ${x} ${y} L ${nextX} ${nextY} z `;
|
||||
});
|
||||
|
||||
seriesGroup
|
||||
.append("path")
|
||||
.attr("d", path)
|
||||
.attr("fill", "none")
|
||||
.attr("stroke", seriesColours[seriesIndex % seriesColours.length])
|
||||
.attr("stroke-width", "1");
|
||||
|
||||
xValues.forEach(x => {
|
||||
const y = serie.data[x];
|
||||
if (typeof y === "undefined") return;
|
||||
|
||||
seriesGroup
|
||||
.append("circle")
|
||||
.attr("cx", xAxis(x))
|
||||
.attr("cy", yAxis(y))
|
||||
.attr("r", pipRadius)
|
||||
.attr("fill", seriesColours[seriesIndex % seriesColours.length])
|
||||
.append("title")
|
||||
.text(d => {
|
||||
return `${x}\n
|
||||
--\n
|
||||
${tooltipText[x]}\n
|
||||
`.replace(/\s{2,}/g, "\n");
|
||||
});
|
||||
});
|
||||
|
||||
yAxesArea
|
||||
.append("g")
|
||||
.attr("transform", `translate(${seriesLabelWidth - interSeriesPadding}, ${seriesHeight * seriesIndex + interSeriesPadding * (seriesIndex + 1)})`)
|
||||
.attr("class", "axis axis--y")
|
||||
.call(d3.axisLeft(yAxis).ticks(5));
|
||||
|
||||
yAxesArea
|
||||
.append("g")
|
||||
.attr("transform", `translate(0, ${seriesHeight / 2 + seriesHeight * seriesIndex + interSeriesPadding * (seriesIndex + 1)})`)
|
||||
.append("text")
|
||||
.style("text-anchor", "middle")
|
||||
.attr("transform", "rotate(-90)")
|
||||
.text(serie.name);
|
||||
});
|
||||
|
||||
return svg._groups[0][0].outerHTML;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default SeriesChart;
|
||||
@@ -7,7 +7,7 @@
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import Utils from "../Utils";
|
||||
import Magic from "../lib/Magic";
|
||||
import {isImage} from "../lib/FileType";
|
||||
|
||||
import jimp from "jimp";
|
||||
|
||||
@@ -38,56 +38,53 @@ class SplitColourChannels extends Operation {
|
||||
* @returns {List<File>}
|
||||
*/
|
||||
async run(input, args) {
|
||||
const type = Magic.magicFileType(input);
|
||||
// Make sure that the input is an image
|
||||
if (type && type.mime.indexOf("image") === 0) {
|
||||
const parsedImage = await jimp.read(Buffer.from(input));
|
||||
if (!isImage(input)) throw new OperationError("Invalid file type.");
|
||||
|
||||
const red = new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const split = parsedImage
|
||||
.clone()
|
||||
.color([
|
||||
{apply: "blue", params: [-255]},
|
||||
{apply: "green", params: [-255]}
|
||||
])
|
||||
.getBufferAsync(jimp.MIME_PNG);
|
||||
resolve(new File([new Uint8Array((await split).values())], "red.png", {type: "image/png"}));
|
||||
} catch (err) {
|
||||
reject(new OperationError(`Could not split red channel: ${err}`));
|
||||
}
|
||||
});
|
||||
const parsedImage = await jimp.read(Buffer.from(input));
|
||||
|
||||
const green = new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const split = parsedImage.clone()
|
||||
.color([
|
||||
{apply: "red", params: [-255]},
|
||||
{apply: "blue", params: [-255]},
|
||||
]).getBufferAsync(jimp.MIME_PNG);
|
||||
resolve(new File([new Uint8Array((await split).values())], "green.png", {type: "image/png"}));
|
||||
} catch (err) {
|
||||
reject(new OperationError(`Could not split green channel: ${err}`));
|
||||
}
|
||||
});
|
||||
const red = new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const split = parsedImage
|
||||
.clone()
|
||||
.color([
|
||||
{apply: "blue", params: [-255]},
|
||||
{apply: "green", params: [-255]}
|
||||
])
|
||||
.getBufferAsync(jimp.MIME_PNG);
|
||||
resolve(new File([new Uint8Array((await split).values())], "red.png", {type: "image/png"}));
|
||||
} catch (err) {
|
||||
reject(new OperationError(`Could not split red channel: ${err}`));
|
||||
}
|
||||
});
|
||||
|
||||
const blue = new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const split = parsedImage
|
||||
.color([
|
||||
{apply: "red", params: [-255]},
|
||||
{apply: "green", params: [-255]},
|
||||
]).getBufferAsync(jimp.MIME_PNG);
|
||||
resolve(new File([new Uint8Array((await split).values())], "blue.png", {type: "image/png"}));
|
||||
} catch (err) {
|
||||
reject(new OperationError(`Could not split blue channel: ${err}`));
|
||||
}
|
||||
});
|
||||
const green = new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const split = parsedImage.clone()
|
||||
.color([
|
||||
{apply: "red", params: [-255]},
|
||||
{apply: "blue", params: [-255]},
|
||||
]).getBufferAsync(jimp.MIME_PNG);
|
||||
resolve(new File([new Uint8Array((await split).values())], "green.png", {type: "image/png"}));
|
||||
} catch (err) {
|
||||
reject(new OperationError(`Could not split green channel: ${err}`));
|
||||
}
|
||||
});
|
||||
|
||||
return await Promise.all([red, green, blue]);
|
||||
} else {
|
||||
throw new OperationError("Invalid file type.");
|
||||
}
|
||||
const blue = new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const split = parsedImage
|
||||
.color([
|
||||
{apply: "red", params: [-255]},
|
||||
{apply: "green", params: [-255]},
|
||||
]).getBufferAsync(jimp.MIME_PNG);
|
||||
resolve(new File([new Uint8Array((await split).values())], "blue.png", {type: "image/png"}));
|
||||
} catch (err) {
|
||||
reject(new OperationError(`Could not split blue channel: ${err}`));
|
||||
}
|
||||
});
|
||||
|
||||
return await Promise.all([red, green, blue]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -116,7 +116,7 @@ class Subsection extends Operation {
|
||||
}
|
||||
|
||||
// Baseline ing values for each tranche so that registers are reset
|
||||
subOpList.forEach((op, i) => {
|
||||
recipe.opList.forEach((op, i) => {
|
||||
op.ingValues = JSON.parse(JSON.stringify(ingValues[i]));
|
||||
});
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ class TextEncodingBruteForce extends Operation {
|
||||
let table = "<table class='table table-hover table-sm table-bordered table-nonfluid'><tr><th>Encoding</th><th>Value</th></tr>";
|
||||
|
||||
for (const enc in encodings) {
|
||||
const value = Utils.printable(encodings[enc], true);
|
||||
const value = Utils.escapeHtml(Utils.printable(encodings[enc], true));
|
||||
table += `<tr><td>${enc}</td><td>${value}</td></tr>`;
|
||||
}
|
||||
|
||||
|
||||
250
src/core/operations/Typex.mjs
Normal file
250
src/core/operations/Typex.mjs
Normal file
@@ -0,0 +1,250 @@
|
||||
/**
|
||||
* Emulation of the Typex machine.
|
||||
*
|
||||
* @author s2224834
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import {LETTERS, Reflector} from "../lib/Enigma";
|
||||
import {ROTORS, REFLECTORS, TypexMachine, Plugboard, Rotor} from "../lib/Typex";
|
||||
|
||||
/**
|
||||
* Typex operation
|
||||
*/
|
||||
class Typex extends Operation {
|
||||
/**
|
||||
* Typex constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "Typex";
|
||||
this.module = "Default";
|
||||
this.description = "Encipher/decipher with the WW2 Typex machine.<br><br>Typex was originally built by the British Royal Air Force prior to WW2, and is based on the Enigma machine with some improvements made, including using five rotors with more stepping points and interchangeable wiring cores. It was used across the British and Commonewealth militaries. A number of later variants were produced; here we simulate a WW2 era Mark 22 Typex with plugboards for the reflector and input. Typex rotors were changed regularly and none are public: a random example set are provided.<br><br>To configure the reflector plugboard, enter a string of connected pairs of letters in the reflector box, e.g. <code>AB CD EF</code> connects A to B, C to D, and E to F (you'll need to connect every letter). There is also an input plugboard: unlike Enigma's plugboard, it's not restricted to pairs, so it's entered like a rotor (without stepping). To create your own rotor, enter the letters that the rotor maps A to Z to, in order, optionally followed by <code><</code> then a list of stepping points.<br><br>More detailed descriptions of the Enigma, Typex and Bombe operations <a href='https://github.com/gchq/CyberChef/wiki/Enigma,-the-Bombe,-and-Typex'>can be found here</a>.";
|
||||
this.infoURL = "https://wikipedia.org/wiki/Typex";
|
||||
this.inputType = "string";
|
||||
this.outputType = "string";
|
||||
this.args = [
|
||||
{
|
||||
name: "1st (left-hand) rotor",
|
||||
type: "editableOption",
|
||||
value: ROTORS,
|
||||
defaultIndex: 0
|
||||
},
|
||||
{
|
||||
name: "1st rotor reversed",
|
||||
type: "boolean",
|
||||
value: false
|
||||
},
|
||||
{
|
||||
name: "1st rotor ring setting",
|
||||
type: "option",
|
||||
value: LETTERS
|
||||
},
|
||||
{
|
||||
name: "1st rotor initial value",
|
||||
type: "option",
|
||||
value: LETTERS
|
||||
},
|
||||
{
|
||||
name: "2nd rotor",
|
||||
type: "editableOption",
|
||||
value: ROTORS,
|
||||
defaultIndex: 1
|
||||
},
|
||||
{
|
||||
name: "2nd rotor reversed",
|
||||
type: "boolean",
|
||||
value: false
|
||||
},
|
||||
{
|
||||
name: "2nd rotor ring setting",
|
||||
type: "option",
|
||||
value: LETTERS
|
||||
},
|
||||
{
|
||||
name: "2nd rotor initial value",
|
||||
type: "option",
|
||||
value: LETTERS
|
||||
},
|
||||
{
|
||||
name: "3rd (middle) rotor",
|
||||
type: "editableOption",
|
||||
value: ROTORS,
|
||||
defaultIndex: 2
|
||||
},
|
||||
{
|
||||
name: "3rd rotor reversed",
|
||||
type: "boolean",
|
||||
value: false
|
||||
},
|
||||
{
|
||||
name: "3rd rotor ring setting",
|
||||
type: "option",
|
||||
value: LETTERS
|
||||
},
|
||||
{
|
||||
name: "3rd rotor initial value",
|
||||
type: "option",
|
||||
value: LETTERS
|
||||
},
|
||||
{
|
||||
name: "4th (static) rotor",
|
||||
type: "editableOption",
|
||||
value: ROTORS,
|
||||
defaultIndex: 3
|
||||
},
|
||||
{
|
||||
name: "4th rotor reversed",
|
||||
type: "boolean",
|
||||
value: false
|
||||
},
|
||||
{
|
||||
name: "4th rotor ring setting",
|
||||
type: "option",
|
||||
value: LETTERS
|
||||
},
|
||||
{
|
||||
name: "4th rotor initial value",
|
||||
type: "option",
|
||||
value: LETTERS
|
||||
},
|
||||
{
|
||||
name: "5th (right-hand, static) rotor",
|
||||
type: "editableOption",
|
||||
value: ROTORS,
|
||||
defaultIndex: 4
|
||||
},
|
||||
{
|
||||
name: "5th rotor reversed",
|
||||
type: "boolean",
|
||||
value: false
|
||||
},
|
||||
{
|
||||
name: "5th rotor ring setting",
|
||||
type: "option",
|
||||
value: LETTERS
|
||||
},
|
||||
{
|
||||
name: "5th rotor initial value",
|
||||
type: "option",
|
||||
value: LETTERS
|
||||
},
|
||||
{
|
||||
name: "Reflector",
|
||||
type: "editableOption",
|
||||
value: REFLECTORS
|
||||
},
|
||||
{
|
||||
name: "Plugboard",
|
||||
type: "string",
|
||||
value: ""
|
||||
},
|
||||
{
|
||||
name: "Typex keyboard emulation",
|
||||
type: "option",
|
||||
value: ["None", "Encrypt", "Decrypt"]
|
||||
},
|
||||
{
|
||||
name: "Strict output",
|
||||
hint: "Remove non-alphabet letters and group output",
|
||||
type: "boolean",
|
||||
value: true
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper - for ease of use rotors are specified as a single string; this
|
||||
* method breaks the spec string into wiring and steps parts.
|
||||
*
|
||||
* @param {string} rotor - Rotor specification string.
|
||||
* @param {number} i - For error messages, the number of this rotor.
|
||||
* @returns {string[]}
|
||||
*/
|
||||
parseRotorStr(rotor, i) {
|
||||
if (rotor === "") {
|
||||
throw new OperationError(`Rotor ${i} must be provided.`);
|
||||
}
|
||||
if (!rotor.includes("<")) {
|
||||
return [rotor, ""];
|
||||
}
|
||||
return rotor.split("<", 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} input
|
||||
* @param {Object[]} args
|
||||
* @returns {string}
|
||||
*/
|
||||
run(input, args) {
|
||||
const reflectorstr = args[20];
|
||||
const plugboardstr = args[21];
|
||||
const typexKeyboard = args[22];
|
||||
const removeOther = args[23];
|
||||
const rotors = [];
|
||||
for (let i=0; i<5; i++) {
|
||||
const [rotorwiring, rotorsteps] = this.parseRotorStr(args[i*4]);
|
||||
rotors.push(new Rotor(rotorwiring, rotorsteps, args[i*4 + 1], args[i*4+2], args[i*4+3]));
|
||||
}
|
||||
// Rotors are handled in reverse
|
||||
rotors.reverse();
|
||||
const reflector = new Reflector(reflectorstr);
|
||||
let plugboardstrMod = plugboardstr;
|
||||
if (plugboardstrMod === "") {
|
||||
plugboardstrMod = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
}
|
||||
const plugboard = new Plugboard(plugboardstrMod);
|
||||
if (removeOther) {
|
||||
if (typexKeyboard === "Encrypt") {
|
||||
input = input.replace(/[^A-Za-z0-9 /%£()',.-]/g, "");
|
||||
} else {
|
||||
input = input.replace(/[^A-Za-z]/g, "");
|
||||
}
|
||||
}
|
||||
const typex = new TypexMachine(rotors, reflector, plugboard, typexKeyboard);
|
||||
let result = typex.crypt(input);
|
||||
if (removeOther && typexKeyboard !== "Decrypt") {
|
||||
// Five character cipher groups is traditional
|
||||
result = result.replace(/([A-Z]{5})(?!$)/g, "$1 ");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlight Typex
|
||||
* This is only possible if we're passing through non-alphabet characters.
|
||||
*
|
||||
* @param {Object[]} pos
|
||||
* @param {number} pos[].start
|
||||
* @param {number} pos[].end
|
||||
* @param {Object[]} args
|
||||
* @returns {Object[]} pos
|
||||
*/
|
||||
highlight(pos, args) {
|
||||
if (args[18] === false) {
|
||||
return pos;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlight Typex in reverse
|
||||
*
|
||||
* @param {Object[]} pos
|
||||
* @param {number} pos[].start
|
||||
* @param {number} pos[].end
|
||||
* @param {Object[]} args
|
||||
* @returns {Object[]} pos
|
||||
*/
|
||||
highlightReverse(pos, args) {
|
||||
if (args[18] === false) {
|
||||
return pos;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default Typex;
|
||||
@@ -20,7 +20,7 @@ class UnescapeString extends Operation {
|
||||
|
||||
this.name = "Unescape string";
|
||||
this.module = "Default";
|
||||
this.description = "Unescapes characters in a string that have been escaped. For example, <code>Don\\'t stop me now</code> becomes <code>Don't stop me now</code>.<br><br>Supports the following escape sequences:<ul><li><code>\\n</code> (Line feed/newline)</li><li><code>\\r</code> (Carriage return)</li><li><code>\\t</code> (Horizontal tab)</li><li><code>\\b</code> (Backspace)</li><li><code>\\f</code> (Form feed)</li><li><code>\\xnn</code> (Hex, where n is 0-f)</li><li><code>\\\\</code> (Backslash)</li><li><code>\\'</code> (Single quote)</li><li><code>\\"</code> (Double quote)</li><li><code>\\unnnn</code> (Unicode character)</li><li><code>\\u{nnnnnn}</code> (Unicode code point)</li></ul>";
|
||||
this.description = "Unescapes characters in a string that have been escaped. For example, <code>Don\\'t stop me now</code> becomes <code>Don't stop me now</code>.<br><br>Supports the following escape sequences:<ul><li><code>\\n</code> (Line feed/newline)</li><li><code>\\r</code> (Carriage return)</li><li><code>\\t</code> (Horizontal tab)</li><li><code>\\b</code> (Backspace)</li><li><code>\\f</code> (Form feed)</li><li><code>\\nnn</code> (Octal, where n is 0-7)</li><li><code>\\xnn</code> (Hex, where n is 0-f)</li><li><code>\\\\</code> (Backslash)</li><li><code>\\'</code> (Single quote)</li><li><code>\\"</code> (Double quote)</li><li><code>\\unnnn</code> (Unicode character)</li><li><code>\\u{nnnnnn}</code> (Unicode code point)</li></ul>";
|
||||
this.infoURL = "https://wikipedia.org/wiki/Escape_sequence";
|
||||
this.inputType = "string";
|
||||
this.outputType = "string";
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import Operation from "../Operation";
|
||||
import Utils from "../Utils";
|
||||
import Stream from "../lib/Stream";
|
||||
|
||||
/**
|
||||
* Untar operation
|
||||
@@ -41,38 +42,6 @@ class Untar extends Operation {
|
||||
* @returns {List<File>}
|
||||
*/
|
||||
run(input, args) {
|
||||
const Stream = function(input) {
|
||||
this.bytes = input;
|
||||
this.position = 0;
|
||||
};
|
||||
|
||||
Stream.prototype.getBytes = function(bytesToGet) {
|
||||
const newPosition = this.position + bytesToGet;
|
||||
const bytes = this.bytes.slice(this.position, newPosition);
|
||||
this.position = newPosition;
|
||||
return bytes;
|
||||
};
|
||||
|
||||
Stream.prototype.readString = function(numBytes) {
|
||||
let result = "";
|
||||
for (let i = this.position; i < this.position + numBytes; i++) {
|
||||
const currentByte = this.bytes[i];
|
||||
if (currentByte === 0) break;
|
||||
result += String.fromCharCode(currentByte);
|
||||
}
|
||||
this.position += numBytes;
|
||||
return result;
|
||||
};
|
||||
|
||||
Stream.prototype.readInt = function(numBytes, base) {
|
||||
const string = this.readString(numBytes);
|
||||
return parseInt(string, base);
|
||||
};
|
||||
|
||||
Stream.prototype.hasMore = function() {
|
||||
return this.position < this.bytes.length;
|
||||
};
|
||||
|
||||
const stream = new Stream(input),
|
||||
files = [];
|
||||
|
||||
@@ -85,7 +54,7 @@ class Untar extends Operation {
|
||||
ownerUID: stream.readString(8),
|
||||
ownerGID: stream.readString(8),
|
||||
size: parseInt(stream.readString(12), 8), // Octal
|
||||
lastModTime: new Date(1000 * stream.readInt(12, 8)), // Octal
|
||||
lastModTime: new Date(1000 * parseInt(stream.readString(12), 8)), // Octal
|
||||
checksum: stream.readString(8),
|
||||
type: stream.readString(1),
|
||||
linkedFileName: stream.readString(100),
|
||||
|
||||
46
src/core/operations/VarIntDecode.mjs
Normal file
46
src/core/operations/VarIntDecode.mjs
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* @author GCHQ Contributor [3]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import Protobuf from "../lib/Protobuf";
|
||||
|
||||
/**
|
||||
* VarInt Decode operation
|
||||
*/
|
||||
class VarIntDecode extends Operation {
|
||||
|
||||
/**
|
||||
* VarIntDecode constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "VarInt Decode";
|
||||
this.module = "Default";
|
||||
this.description = "Decodes a VarInt encoded integer. VarInt is an efficient way of encoding variable length integers and is commonly used with Protobuf.";
|
||||
this.infoURL = "https://developers.google.com/protocol-buffers/docs/encoding#varints";
|
||||
this.inputType = "byteArray";
|
||||
this.outputType = "number";
|
||||
this.args = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {byteArray} input
|
||||
* @param {Object[]} args
|
||||
* @returns {number}
|
||||
*/
|
||||
run(input, args) {
|
||||
try {
|
||||
return Protobuf.varIntDecode(input);
|
||||
} catch (err) {
|
||||
throw new OperationError(err);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default VarIntDecode;
|
||||
46
src/core/operations/VarIntEncode.mjs
Normal file
46
src/core/operations/VarIntEncode.mjs
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* @author GCHQ Contributor [3]
|
||||
* @copyright Crown Copyright 2019
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
|
||||
import Operation from "../Operation";
|
||||
import OperationError from "../errors/OperationError";
|
||||
import Protobuf from "../lib/Protobuf";
|
||||
|
||||
/**
|
||||
* VarInt Encode operation
|
||||
*/
|
||||
class VarIntEncode extends Operation {
|
||||
|
||||
/**
|
||||
* VarIntEncode constructor
|
||||
*/
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.name = "VarInt Encode";
|
||||
this.module = "Default";
|
||||
this.description = "Encodes a Vn integer as a VarInt. VarInt is an efficient way of encoding variable length integers and is commonly used with Protobuf.";
|
||||
this.infoURL = "https://developers.google.com/protocol-buffers/docs/encoding#varints";
|
||||
this.inputType = "number";
|
||||
this.outputType = "byteArray";
|
||||
this.args = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} input
|
||||
* @param {Object[]} args
|
||||
* @returns {byteArray}
|
||||
*/
|
||||
run(input, args) {
|
||||
try {
|
||||
return Protobuf.varIntEncode(input);
|
||||
} catch (err) {
|
||||
throw new OperationError(err);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default VarIntEncode;
|
||||
@@ -57,7 +57,7 @@ class XPathExpression extends Operation {
|
||||
|
||||
let nodes;
|
||||
try {
|
||||
nodes = xpath.select(query, doc);
|
||||
nodes = xpath.parse(query).select({ node: doc, allowAnyNamespaceForNoPrefix: true });
|
||||
} catch (err) {
|
||||
throw new OperationError(`Invalid XPath. Details:\n${err.message}.`);
|
||||
}
|
||||
|
||||
99
src/core/vendor/DisassembleX86-64.mjs
vendored
99
src/core/vendor/DisassembleX86-64.mjs
vendored
@@ -1,3 +1,31 @@
|
||||
/*-------------------------------------------------------------------------------------------------------------------------
|
||||
Created by Damian Recoskie (https://github.com/Recoskie/X86-64-Disassembler-JS)
|
||||
& exported for CyberChef by Matt [me@mitt.dev]
|
||||
---------------------------------------------------------------------------------------------------------------------------
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Damian Recoskie
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
-------------------------------------------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
/*-------------------------------------------------------------------------------------------------------------------------
|
||||
Binary byte code array.
|
||||
---------------------------------------------------------------------------------------------------------------------------
|
||||
@@ -3525,7 +3553,7 @@ export function LoadBinCode( HexStr )
|
||||
|
||||
var len = HexStr.length;
|
||||
|
||||
for( var i = 0, el = 0, Sing = 0, int32 = 0; i < len; i += 8 )
|
||||
for( var i = 0, el = 0, Sign = 0, int32 = 0; i < len; i += 8 )
|
||||
{
|
||||
//It is faster to read 8 hex digits at a time if possible.
|
||||
|
||||
@@ -3541,22 +3569,22 @@ export function LoadBinCode( HexStr )
|
||||
|
||||
//The variable sing corrects the unusable sing bits during the 4 byte rotation algorithm.
|
||||
|
||||
Sing = int32;
|
||||
Sign = int32;
|
||||
|
||||
//Remove the Sing bit value if active for when the number is changed to int32 during rotation.
|
||||
//Remove the Sign bit value if active for when the number is changed to int32 during rotation.
|
||||
|
||||
int32 ^= int32 & 0x80000000;
|
||||
|
||||
//Rotate the 32 bit int so that each number is put in order in the BinCode array. Add the Sing Bit positions back though each rotation.
|
||||
//Rotate the 32 bit int so that each number is put in order in the BinCode array. Add the Sign Bit positions back though each rotation.
|
||||
|
||||
int32 = ( int32 >> 24 ) | ( ( int32 << 8 ) & 0x7FFFFFFF );
|
||||
BinCode[el++] = ( ( ( Sing >> 24 ) & 0x80 ) | int32 ) & 0xFF;
|
||||
BinCode[el++] = ( ( ( Sign >> 24 ) & 0x80 ) | int32 ) & 0xFF;
|
||||
int32 = ( int32 >> 24 ) | ( ( int32 << 8 ) & 0x7FFFFFFF );
|
||||
BinCode[el++] = ( ( ( Sing >> 16 ) & 0x80 ) | int32 ) & 0xFF;
|
||||
BinCode[el++] = ( ( ( Sign >> 16 ) & 0x80 ) | int32 ) & 0xFF;
|
||||
int32 = ( int32 >> 24 ) | ( ( int32 << 8 ) & 0x7FFFFFFF );
|
||||
BinCode[el++] = ( ( ( Sing >> 8 ) & 0x80 ) | int32 ) & 0xFF;
|
||||
BinCode[el++] = ( ( ( Sign >> 8 ) & 0x80 ) | int32 ) & 0xFF;
|
||||
int32 = ( int32 >> 24 ) | ( ( int32 << 8 ) & 0x7FFFFFFF );
|
||||
BinCode[el++] = ( ( Sing & 0x80 ) | int32 ) & 0xFF;
|
||||
BinCode[el++] = ( ( Sign & 0x80 ) | int32 ) & 0xFF;
|
||||
}
|
||||
|
||||
//Remove elements past the Number of bytes in HexStr because int 32 is always 4 bytes it is possible to end in an uneven number.
|
||||
@@ -3581,11 +3609,10 @@ function NextByte()
|
||||
{
|
||||
//Add the current byte as hex to InstructionHex which will be displayed beside the decoded instruction.
|
||||
//After an instruction decodes InstructionHex is only added beside the instruction if ShowInstructionHex is active.
|
||||
|
||||
var t;
|
||||
if ( CodePos < BinCode.length ) //If not out of bounds.
|
||||
{
|
||||
//Convert current byte to String, and pad.
|
||||
var t;
|
||||
|
||||
( ( t = BinCode[CodePos++].toString(16) ).length === 1) && ( t = "0" + t );
|
||||
|
||||
@@ -3947,11 +3974,11 @@ function DecodeImmediate( type, BySize, SizeSetting )
|
||||
|
||||
var Pad32 = 0, Pad64 = 0;
|
||||
|
||||
//*Initialize the Sing value that is only set for Negative, or Positive Relative displacements.
|
||||
//*Initialize the Sign value that is only set for Negative, or Positive Relative displacements.
|
||||
|
||||
var Sing = 0;
|
||||
var Sign = 0;
|
||||
|
||||
//*Initialize the Sing Extend variable size as 0 Some Immediate numbers Sing extend.
|
||||
//*Initialize the Sign Extend variable size as 0 Some Immediate numbers Sign extend.
|
||||
|
||||
var Extend = 0;
|
||||
|
||||
@@ -4017,21 +4044,33 @@ function DecodeImmediate( type, BySize, SizeSetting )
|
||||
|
||||
Pad32 = ( Math.min( BitMode, 1 ) << 2 ) + 4; Pad64 = Math.max( Math.min( BitMode, 2 ), 1 ) << 3;
|
||||
|
||||
//Add the 32 bit section to V32.
|
||||
//Carry bit to 64 bit section.
|
||||
|
||||
var C64 = 0;
|
||||
|
||||
//Relative size.
|
||||
|
||||
var n = Math.min( 0x100000000, Math.pow( 2, 4 << ( S + 1 ) ) );
|
||||
|
||||
//Sign bit adjust.
|
||||
|
||||
if( V32 >= ( n >> 1 ) ) { V32 -= n; }
|
||||
|
||||
//Add position.
|
||||
|
||||
V32 += Pos32;
|
||||
|
||||
//Remove carry bit and add it to C64.
|
||||
|
||||
var C64 = 0; V32 += Pos32;
|
||||
( C64 = ( ( V32 ) >= 0x100000000 ) ) && ( V32 -= 0x100000000 );
|
||||
|
||||
//Do not carry to 64 if address is 32, and below.
|
||||
|
||||
if ( S <= 2 ) { C64 = false; }
|
||||
|
||||
//If bit mode is 16 bits only the first 16 bits are used, or if Size Attribute is 16 bit.
|
||||
//Add the 64 bit position plus carry.
|
||||
|
||||
( BitMode <= 0 || SizeAttrSelect <= 0 ) && ( V32 &= 0xFFFF );
|
||||
|
||||
//Adjust the 32 bit relative address section if it was not cropped to 16 bit's.
|
||||
|
||||
( C64 = ( ( V32 ) > 0xFFFFFFFF ) ) && ( V32 -= 0x100000000 );
|
||||
|
||||
//Add the 64 bit address section if in 64 bit mode, or higher.
|
||||
|
||||
( BitMode >= 2 ) && ( ( V64 += Pos64 + C64 ) > 0xFFFFFFFF ) && ( V64 -= 0x100000000 );
|
||||
( ( V64 += Pos64 + C64 ) > 0xFFFFFFFF ) && ( V64 -= 0x100000000 );
|
||||
}
|
||||
|
||||
/*---------------------------------------------------------------------------------------------------------------------------
|
||||
@@ -4052,9 +4091,9 @@ function DecodeImmediate( type, BySize, SizeSetting )
|
||||
|
||||
var Center = 2 * ( 1 << ( n << 3 ) - 2 );
|
||||
|
||||
//By default the Sing is Positive.
|
||||
//By default the Sign is Positive.
|
||||
|
||||
Sing = 1;
|
||||
Sign = 1;
|
||||
|
||||
/*-------------------------------------------------------------------------------------------------------------------------
|
||||
Calculate the VSIB displacement size if it is a VSIB Disp8.
|
||||
@@ -4074,9 +4113,9 @@ function DecodeImmediate( type, BySize, SizeSetting )
|
||||
|
||||
V32 = Center * 2 - V32;
|
||||
|
||||
//The Sing is negative.
|
||||
//The Sign is negative.
|
||||
|
||||
Sing = 2;
|
||||
Sign = 2;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4110,7 +4149,7 @@ function DecodeImmediate( type, BySize, SizeSetting )
|
||||
|
||||
//*Return the Imm.
|
||||
|
||||
return ( ( Sing > 0 ? ( Sing > 1 ? "-" : "+" ) : "" ) + Imm.toUpperCase() );
|
||||
return ( ( Sign > 0 ? ( Sign > 1 ? "-" : "+" ) : "" ) + Imm.toUpperCase() );
|
||||
|
||||
}
|
||||
|
||||
|
||||
265
src/core/vendor/bzip2.mjs
vendored
265
src/core/vendor/bzip2.mjs
vendored
@@ -1,265 +0,0 @@
|
||||
/** @license
|
||||
========================================================================
|
||||
bzip2.js - a small bzip2 decompression implementation
|
||||
|
||||
Copyright 2011 by antimatter15 (antimatter15@gmail.com)
|
||||
|
||||
Based on micro-bunzip by Rob Landley (rob@landley.net).
|
||||
|
||||
Copyright (c) 2011 by antimatter15 (antimatter15@gmail.com).
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH
|
||||
THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
var bzip2 = {};
|
||||
|
||||
bzip2.array = function(bytes){
|
||||
var bit = 0, byte = 0;
|
||||
var BITMASK = [0, 0x01, 0x03, 0x07, 0x0F, 0x1F, 0x3F, 0x7F, 0xFF ];
|
||||
return function(n){
|
||||
var result = 0;
|
||||
while(n > 0){
|
||||
var left = 8 - bit;
|
||||
if(n >= left){
|
||||
result <<= left;
|
||||
result |= (BITMASK[left] & bytes[byte++]);
|
||||
bit = 0;
|
||||
n -= left;
|
||||
}else{
|
||||
result <<= n;
|
||||
result |= ((bytes[byte] & (BITMASK[n] << (8 - n - bit))) >> (8 - n - bit));
|
||||
bit += n;
|
||||
n = 0;
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
bzip2.simple = function(bits){
|
||||
var size = bzip2.header(bits);
|
||||
var all = '', chunk = '';
|
||||
do{
|
||||
all += chunk;
|
||||
chunk = bzip2.decompress(bits, size);
|
||||
}while(chunk != -1);
|
||||
return all;
|
||||
}
|
||||
|
||||
bzip2.header = function(bits){
|
||||
if(bits(8*3) != 4348520) throw "No magic number found";
|
||||
var i = bits(8) - 48;
|
||||
if(i < 1 || i > 9) throw "Not a BZIP archive";
|
||||
return i;
|
||||
};
|
||||
|
||||
|
||||
//takes a function for reading the block data (starting with 0x314159265359)
|
||||
//a block size (0-9) (optional, defaults to 9)
|
||||
//a length at which to stop decompressing and return the output
|
||||
bzip2.decompress = function(bits, size, len){
|
||||
var MAX_HUFCODE_BITS = 20;
|
||||
var MAX_SYMBOLS = 258;
|
||||
var SYMBOL_RUNA = 0;
|
||||
var SYMBOL_RUNB = 1;
|
||||
var GROUP_SIZE = 50;
|
||||
|
||||
var bufsize = 100000 * size;
|
||||
for(var h = '', i = 0; i < 6; i++) h += bits(8).toString(16);
|
||||
if(h == "177245385090") return -1; //last block
|
||||
if(h != "314159265359") throw "Not valid bzip data";
|
||||
bits(32); //ignore CRC codes
|
||||
if(bits(1)) throw "Unsupported obsolete version";
|
||||
var origPtr = bits(24);
|
||||
if(origPtr > bufsize) throw "Initial position larger than buffer size";
|
||||
var t = bits(16);
|
||||
var symToByte = new Uint8Array(256),
|
||||
symTotal = 0;
|
||||
for (i = 0; i < 16; i++) {
|
||||
if(t & (1 << (15 - i))) {
|
||||
var k = bits(16);
|
||||
for(j = 0; j < 16; j++){
|
||||
if(k & (1 << (15 - j))){
|
||||
symToByte[symTotal++] = (16 * i) + j;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var groupCount = bits(3);
|
||||
if(groupCount < 2 || groupCount > 6) throw "Error 1";
|
||||
var nSelectors = bits(15);
|
||||
if(nSelectors == 0) throw "Error";
|
||||
var mtfSymbol = []; //TODO: possibly replace JS array with typed arrays
|
||||
for(var i = 0; i < groupCount; i++) mtfSymbol[i] = i;
|
||||
var selectors = new Uint8Array(32768);
|
||||
|
||||
for(var i = 0; i < nSelectors; i++){
|
||||
for(var j = 0; bits(1); j++) if(j >= groupCount) throw "Error 2";
|
||||
var uc = mtfSymbol[j];
|
||||
mtfSymbol.splice(j, 1); //this is a probably inefficient MTF transform
|
||||
mtfSymbol.splice(0, 0, uc);
|
||||
selectors[i] = uc;
|
||||
}
|
||||
|
||||
var symCount = symTotal + 2;
|
||||
var groups = [];
|
||||
for(var j = 0; j < groupCount; j++){
|
||||
var length = new Uint8Array(MAX_SYMBOLS),
|
||||
temp = new Uint8Array(MAX_HUFCODE_BITS+1);
|
||||
t = bits(5); //lengths
|
||||
for(var i = 0; i < symCount; i++){
|
||||
while(true){
|
||||
if (t < 1 || t > MAX_HUFCODE_BITS) throw "Error 3";
|
||||
if(!bits(1)) break;
|
||||
if(!bits(1)) t++;
|
||||
else t--;
|
||||
}
|
||||
length[i] = t;
|
||||
}
|
||||
var minLen, maxLen;
|
||||
minLen = maxLen = length[0];
|
||||
for(var i = 1; i < symCount; i++){
|
||||
if(length[i] > maxLen) maxLen = length[i];
|
||||
else if(length[i] < minLen) minLen = length[i];
|
||||
}
|
||||
var hufGroup;
|
||||
hufGroup = groups[j] = {};
|
||||
hufGroup.permute = new Uint32Array(MAX_SYMBOLS);
|
||||
hufGroup.limit = new Uint32Array(MAX_HUFCODE_BITS + 1);
|
||||
hufGroup.base = new Uint32Array(MAX_HUFCODE_BITS + 1);
|
||||
hufGroup.minLen = minLen;
|
||||
hufGroup.maxLen = maxLen;
|
||||
var base = hufGroup.base.subarray(1);
|
||||
var limit = hufGroup.limit.subarray(1);
|
||||
var pp = 0;
|
||||
for(var i = minLen; i <= maxLen; i++)
|
||||
for(var t = 0; t < symCount; t++)
|
||||
if(length[t] == i) hufGroup.permute[pp++] = t;
|
||||
for(i = minLen; i <= maxLen; i++) temp[i] = limit[i] = 0;
|
||||
for(i = 0; i < symCount; i++) temp[length[i]]++;
|
||||
pp = t = 0;
|
||||
for(i = minLen; i < maxLen; i++) {
|
||||
pp += temp[i];
|
||||
limit[i] = pp - 1;
|
||||
pp <<= 1;
|
||||
base[i+1] = pp - (t += temp[i]);
|
||||
}
|
||||
limit[maxLen]=pp+temp[maxLen]-1;
|
||||
base[minLen]=0;
|
||||
}
|
||||
var byteCount = new Uint32Array(256);
|
||||
for(var i = 0; i < 256; i++) mtfSymbol[i] = i;
|
||||
var runPos, count, symCount, selector;
|
||||
runPos = count = symCount = selector = 0;
|
||||
var buf = new Uint32Array(bufsize);
|
||||
while(true){
|
||||
if(!(symCount--)){
|
||||
symCount = GROUP_SIZE - 1;
|
||||
if(selector >= nSelectors) throw "Error 4";
|
||||
hufGroup = groups[selectors[selector++]];
|
||||
base = hufGroup.base.subarray(1);
|
||||
limit = hufGroup.limit.subarray(1);
|
||||
}
|
||||
i = hufGroup.minLen;
|
||||
j = bits(i);
|
||||
while(true){
|
||||
if(i > hufGroup.maxLen) throw "Error 5";
|
||||
if(j <= limit[i]) break;
|
||||
i++;
|
||||
j = (j << 1) | bits(1);
|
||||
}
|
||||
j -= base[i];
|
||||
if(j < 0 || j >= MAX_SYMBOLS) throw "Error 6";
|
||||
var nextSym = hufGroup.permute[j];
|
||||
if (nextSym == SYMBOL_RUNA || nextSym == SYMBOL_RUNB) {
|
||||
if(!runPos){
|
||||
runPos = 1;
|
||||
t = 0;
|
||||
}
|
||||
if(nextSym == SYMBOL_RUNA) t += runPos;
|
||||
else t += 2 * runPos;
|
||||
runPos <<= 1;
|
||||
continue;
|
||||
}
|
||||
if(runPos){
|
||||
runPos = 0;
|
||||
if(count + t >= bufsize) throw "Error 7";
|
||||
uc = symToByte[mtfSymbol[0]];
|
||||
byteCount[uc] += t;
|
||||
while(t--) buf[count++] = uc;
|
||||
}
|
||||
if(nextSym > symTotal) break;
|
||||
if(count >= bufsize) throw "Error 8";
|
||||
i = nextSym -1;
|
||||
uc = mtfSymbol[i];
|
||||
mtfSymbol.splice(i, 1);
|
||||
mtfSymbol.splice(0, 0, uc);
|
||||
uc = symToByte[uc];
|
||||
byteCount[uc]++;
|
||||
buf[count++] = uc;
|
||||
}
|
||||
if(origPtr < 0 || origPtr >= count) throw "Error 9";
|
||||
var j = 0;
|
||||
for(var i = 0; i < 256; i++){
|
||||
k = j + byteCount[i];
|
||||
byteCount[i] = j;
|
||||
j = k;
|
||||
}
|
||||
for(var i = 0; i < count; i++){
|
||||
uc = buf[i] & 0xff;
|
||||
buf[byteCount[uc]] |= (i << 8);
|
||||
byteCount[uc]++;
|
||||
}
|
||||
var pos = 0, current = 0, run = 0;
|
||||
if(count) {
|
||||
pos = buf[origPtr];
|
||||
current = (pos & 0xff);
|
||||
pos >>= 8;
|
||||
run = -1;
|
||||
}
|
||||
count = count;
|
||||
var output = '';
|
||||
var copies, previous, outbyte;
|
||||
if(!len) len = Infinity;
|
||||
while(count){
|
||||
count--;
|
||||
previous = current;
|
||||
pos = buf[pos];
|
||||
current = pos & 0xff;
|
||||
pos >>= 8;
|
||||
if(run++ == 3){
|
||||
copies = current;
|
||||
outbyte = previous;
|
||||
current = -1;
|
||||
}else{
|
||||
copies = 1;
|
||||
outbyte = current;
|
||||
}
|
||||
while(copies--){
|
||||
output += (String.fromCharCode(outbyte));
|
||||
if(!--len) return output;
|
||||
}
|
||||
if(current != previous) run = 0;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
export default bzip2;
|
||||
@@ -5,7 +5,6 @@
|
||||
* @copyright Crown Copyright 2017
|
||||
* @license Apache-2.0
|
||||
*/
|
||||
import "babel-polyfill";
|
||||
|
||||
// Define global environment functions
|
||||
global.ENVIRONMENT_IS_WORKER = function() {
|
||||
|
||||
@@ -51,10 +51,12 @@ class App {
|
||||
*/
|
||||
setup() {
|
||||
document.dispatchEvent(this.manager.appstart);
|
||||
|
||||
this.initialiseSplitter();
|
||||
this.loadLocalStorage();
|
||||
this.populateOperationsList();
|
||||
this.manager.setup();
|
||||
this.manager.output.saveBombe();
|
||||
this.resetLayout();
|
||||
this.setCompileMessage();
|
||||
|
||||
@@ -106,7 +108,7 @@ class App {
|
||||
handleError(err, logToConsole) {
|
||||
if (logToConsole) log.error(err);
|
||||
const msg = err.displayStr || err.toString();
|
||||
this.alert(msg, this.options.errorTimeout, !this.options.showErrors);
|
||||
this.alert(Utils.escapeHtml(msg), this.options.errorTimeout, !this.options.showErrors);
|
||||
}
|
||||
|
||||
|
||||
@@ -122,6 +124,9 @@ class App {
|
||||
// Reset attemptHighlight flag
|
||||
this.options.attemptHighlight = true;
|
||||
|
||||
// Remove all current indicators
|
||||
this.manager.recipe.updateBreakpointIndicator(false);
|
||||
|
||||
this.manager.worker.bake(
|
||||
this.getInput(), // The user's input
|
||||
this.getRecipeConfig(), // The configuration of the recipe
|
||||
@@ -239,7 +244,7 @@ class App {
|
||||
/**
|
||||
* Sets up the adjustable splitter to allow the user to resize areas of the page.
|
||||
*
|
||||
* @param {boolean} [minimise=false] - Set this flag if attempting to minimuse frames to 0 width
|
||||
* @param {boolean} [minimise=false] - Set this flag if attempting to minimise frames to 0 width
|
||||
*/
|
||||
initialiseSplitter(minimise=false) {
|
||||
if (this.columnSplitter) this.columnSplitter.destroy();
|
||||
@@ -247,9 +252,9 @@ class App {
|
||||
|
||||
this.columnSplitter = Split(["#operations", "#recipe", "#IO"], {
|
||||
sizes: [20, 30, 50],
|
||||
minSize: minimise ? [0, 0, 0] : [240, 370, 450],
|
||||
minSize: minimise ? [0, 0, 0] : [240, 310, 450],
|
||||
gutterSize: 4,
|
||||
expandToMin: false,
|
||||
expandToMin: true,
|
||||
onDrag: function() {
|
||||
this.manager.recipe.adjustWidth();
|
||||
}.bind(this)
|
||||
@@ -474,6 +479,7 @@ class App {
|
||||
const item = this.manager.recipe.addOperation(recipeConfig[i].op);
|
||||
|
||||
// Populate arguments
|
||||
log.debug(`Populating arguments for ${recipeConfig[i].op}`);
|
||||
const args = item.querySelectorAll(".arg");
|
||||
for (let j = 0; j < args.length; j++) {
|
||||
if (recipeConfig[i].args[j] === undefined) continue;
|
||||
@@ -499,6 +505,8 @@ class App {
|
||||
item.querySelector(".breakpoint").click();
|
||||
}
|
||||
|
||||
this.manager.recipe.triggerArgEvents(item);
|
||||
|
||||
this.progress = 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -338,7 +338,7 @@ class ControlsWaiter {
|
||||
const saveLink = this.generateStateUrl(true, true, null, "https://gchq.github.io/CyberChef/");
|
||||
|
||||
if (reportBugInfo) {
|
||||
reportBugInfo.innerHTML = `* Version: ${PKG_VERSION + (typeof INLINE === "undefined" ? "" : "s")}
|
||||
reportBugInfo.innerHTML = `* Version: ${PKG_VERSION}
|
||||
* Compile time: ${COMPILE_TIME}
|
||||
* User-Agent:
|
||||
${navigator.userAgent}
|
||||
|
||||
@@ -32,6 +32,9 @@ class HTMLIngredient {
|
||||
this.defaultIndex = config.defaultIndex || 0;
|
||||
this.toggleValues = config.toggleValues;
|
||||
this.id = "ing-" + this.app.nextIngId();
|
||||
this.min = (typeof config.min === "number") ? config.min : "";
|
||||
this.max = (typeof config.max === "number") ? config.max : "";
|
||||
this.step = config.step || 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +45,7 @@ class HTMLIngredient {
|
||||
*/
|
||||
toHtml() {
|
||||
let html = "",
|
||||
i, m;
|
||||
i, m, eventFn;
|
||||
|
||||
switch (this.type) {
|
||||
case "string":
|
||||
@@ -103,6 +106,9 @@ class HTMLIngredient {
|
||||
id="${this.id}"
|
||||
arg-name="${this.name}"
|
||||
value="${this.value}"
|
||||
min="${this.min}"
|
||||
max="${this.max}"
|
||||
step="${this.step}"
|
||||
${this.disabled ? "disabled" : ""}>
|
||||
${this.hint ? "<span class='bmd-help'>" + this.hint + "</span>" : ""}
|
||||
</div>`;
|
||||
@@ -145,10 +151,11 @@ class HTMLIngredient {
|
||||
</div>`;
|
||||
break;
|
||||
case "populateOption":
|
||||
case "populateMultiOption":
|
||||
html += `<div class="form-group">
|
||||
<label for="${this.id}" class="bmd-label-floating">${this.name}</label>
|
||||
<select
|
||||
class="form-control arg"
|
||||
class="form-control arg no-state-change populate-option"
|
||||
id="${this.id}"
|
||||
arg-name="${this.name}"
|
||||
${this.disabled ? "disabled" : ""}>`;
|
||||
@@ -158,14 +165,20 @@ class HTMLIngredient {
|
||||
} else if ((m = this.value[i].name.match(/\[\/([a-z0-9 -()^]+)\]/i))) {
|
||||
html += "</optgroup>";
|
||||
} else {
|
||||
html += `<option populate-value="${Utils.escapeHtml(this.value[i].value)}">${this.value[i].name}</option>`;
|
||||
const val = this.type === "populateMultiOption" ?
|
||||
JSON.stringify(this.value[i].value) :
|
||||
this.value[i].value;
|
||||
html += `<option populate-value='${Utils.escapeHtml(val)}'>${this.value[i].name}</option>`;
|
||||
}
|
||||
}
|
||||
html += `</select>
|
||||
${this.hint ? "<span class='bmd-help'>" + this.hint + "</span>" : ""}
|
||||
</div>`;
|
||||
|
||||
this.manager.addDynamicListener("#" + this.id, "change", this.populateOptionChange, this);
|
||||
eventFn = this.type === "populateMultiOption" ?
|
||||
this.populateMultiOptionChange :
|
||||
this.populateOptionChange;
|
||||
this.manager.addDynamicListener("#" + this.id, "change", eventFn, this);
|
||||
break;
|
||||
case "editableOption":
|
||||
html += `<div class="form-group input-group">
|
||||
@@ -237,6 +250,27 @@ class HTMLIngredient {
|
||||
${this.hint ? "<span class='bmd-help'>" + this.hint + "</span>" : ""}
|
||||
</div>`;
|
||||
break;
|
||||
case "argSelector":
|
||||
html += `<div class="form-group inline">
|
||||
<label for="${this.id}" class="bmd-label-floating inline">${this.name}</label>
|
||||
<select
|
||||
class="form-control arg inline arg-selector"
|
||||
id="${this.id}"
|
||||
arg-name="${this.name}"
|
||||
${this.disabled ? "disabled" : ""}>`;
|
||||
for (i = 0; i < this.value.length; i++) {
|
||||
html += `<option ${this.defaultIndex === i ? "selected" : ""}
|
||||
turnon="${JSON.stringify(this.value[i].on || [])}"
|
||||
turnoff="${JSON.stringify(this.value[i].off || [])}">
|
||||
${this.value[i].name}
|
||||
</option>`;
|
||||
}
|
||||
html += `</select>
|
||||
${this.hint ? "<span class='bmd-help'>" + this.hint + "</span>" : ""}
|
||||
</div>`;
|
||||
|
||||
this.manager.addDynamicListener(".arg-selector", "change", this.argSelectorChange, this);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -252,11 +286,16 @@ class HTMLIngredient {
|
||||
* @param {event} e
|
||||
*/
|
||||
populateOptionChange(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const el = e.target;
|
||||
const op = el.parentNode.parentNode;
|
||||
const target = op.querySelectorAll(".arg")[this.target];
|
||||
|
||||
target.value = el.childNodes[el.selectedIndex].getAttribute("populate-value");
|
||||
const popVal = el.childNodes[el.selectedIndex].getAttribute("populate-value");
|
||||
if (popVal !== "") target.value = popVal;
|
||||
|
||||
const evt = new Event("change");
|
||||
target.dispatchEvent(evt);
|
||||
|
||||
@@ -264,6 +303,37 @@ class HTMLIngredient {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for populate multi option changes.
|
||||
* Populates the relevant arguments with the specified values.
|
||||
*
|
||||
* @param {event} e
|
||||
*/
|
||||
populateMultiOptionChange(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const el = e.target;
|
||||
const op = el.parentNode.parentNode;
|
||||
const args = op.querySelectorAll(".arg");
|
||||
const targets = this.target.map(i => args[i]);
|
||||
const vals = JSON.parse(el.childNodes[el.selectedIndex].getAttribute("populate-value"));
|
||||
const evt = new Event("change");
|
||||
|
||||
for (let i = 0; i < targets.length; i++) {
|
||||
targets[i].value = vals[i];
|
||||
}
|
||||
|
||||
// Fire change event after all targets have been assigned
|
||||
this.manager.recipe.ingChange();
|
||||
|
||||
// Send change event for each target once all have been assigned, to update the label placement.
|
||||
for (const target of targets) {
|
||||
target.dispatchEvent(evt);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for editable option clicks.
|
||||
* Populates the input box with the selected value.
|
||||
@@ -284,6 +354,33 @@ class HTMLIngredient {
|
||||
this.manager.recipe.ingChange();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for argument selector changes.
|
||||
* Shows or hides the relevant arguments for this operation.
|
||||
*
|
||||
* @param {event} e
|
||||
*/
|
||||
argSelectorChange(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const option = e.target.options[e.target.selectedIndex];
|
||||
const op = e.target.closest(".operation");
|
||||
const args = op.querySelectorAll(".ingredients .form-group");
|
||||
const turnon = JSON.parse(option.getAttribute("turnon"));
|
||||
const turnoff = JSON.parse(option.getAttribute("turnoff"));
|
||||
|
||||
args.forEach((arg, i) => {
|
||||
if (turnon.includes(i)) {
|
||||
arg.classList.remove("d-none");
|
||||
}
|
||||
if (turnoff.includes(i)) {
|
||||
arg.classList.add("d-none");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default HTMLIngredient;
|
||||
|
||||
@@ -243,14 +243,22 @@ class InputWaiter {
|
||||
}
|
||||
|
||||
if (file) {
|
||||
this.closeFile();
|
||||
this.loaderWorker = new LoaderWorker();
|
||||
this.loaderWorker.addEventListener("message", this.handleLoaderMessage.bind(this));
|
||||
this.loaderWorker.postMessage({"file": file});
|
||||
this.set(file);
|
||||
this.loadFile(file);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for open input button events
|
||||
* Loads the opened data into the input textarea
|
||||
*
|
||||
* @param {event} e
|
||||
*/
|
||||
inputOpen(e) {
|
||||
e.preventDefault();
|
||||
const file = e.srcElement.files[0];
|
||||
this.loadFile(file);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for messages sent back by the LoaderWorker.
|
||||
@@ -306,6 +314,22 @@ class InputWaiter {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Loads a file into the input.
|
||||
*
|
||||
* @param {File} file
|
||||
*/
|
||||
loadFile(file) {
|
||||
if (file) {
|
||||
this.closeFile();
|
||||
this.loaderWorker = new LoaderWorker();
|
||||
this.loaderWorker.addEventListener("message", this.handleLoaderMessage.bind(this));
|
||||
this.loaderWorker.postMessage({"file": file});
|
||||
this.set(file);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for clear IO events.
|
||||
* Resets the input, output and info areas.
|
||||
|
||||
@@ -146,6 +146,7 @@ class Manager {
|
||||
this.addMultiEventListener("#input-text", "paste", this.input.inputPaste, this.input);
|
||||
document.getElementById("reset-layout").addEventListener("click", this.app.resetLayout.bind(this.app));
|
||||
document.getElementById("clr-io").addEventListener("click", this.input.clearIoClick.bind(this.input));
|
||||
this.addListeners("#open-file", "change", this.input.inputOpen, this.input);
|
||||
this.addListeners("#input-text,#input-file", "dragover", this.input.inputDragover, this.input);
|
||||
this.addListeners("#input-text,#input-file", "dragleave", this.input.inputDragleave, this.input);
|
||||
this.addListeners("#input-text,#input-file", "drop", this.input.inputDrop, this.input);
|
||||
@@ -172,6 +173,7 @@ class Manager {
|
||||
this.addDynamicListener("#output-file-download", "click", this.output.downloadFile, this.output);
|
||||
this.addDynamicListener("#output-file-slice i", "click", this.output.displayFileSlice, this.output);
|
||||
document.getElementById("show-file-overlay").addEventListener("click", this.output.showFileOverlayClick.bind(this.output));
|
||||
this.addDynamicListener(".extract-file,.extract-file i", "click", this.output.extractFileClick, this.output);
|
||||
|
||||
// Options
|
||||
document.getElementById("options").addEventListener("click", this.options.optionsClick.bind(this.options));
|
||||
|
||||
@@ -336,24 +336,54 @@ class OutputWaiter {
|
||||
|
||||
|
||||
/**
|
||||
* Shows or hides the loading icon.
|
||||
* Save bombe object then remove it from the DOM so that it does not cause performance issues.
|
||||
*/
|
||||
saveBombe() {
|
||||
this.bombeEl = document.getElementById("bombe");
|
||||
this.bombeEl.parentNode.removeChild(this.bombeEl);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Shows or hides the output loading screen.
|
||||
* The animated Bombe SVG, whilst quite aesthetically pleasing, is reasonably CPU
|
||||
* intensive, so we remove it from the DOM when not in use. We only show it if the
|
||||
* recipe is taking longer than 200ms. We add it to the DOM just before that so that
|
||||
* it is ready to fade in without stuttering.
|
||||
*
|
||||
* @param {boolean} value
|
||||
* @param {boolean} value - true == show loader
|
||||
*/
|
||||
toggleLoader(value) {
|
||||
clearTimeout(this.appendBombeTimeout);
|
||||
clearTimeout(this.outputLoaderTimeout);
|
||||
|
||||
const outputLoader = document.getElementById("output-loader"),
|
||||
outputElement = document.getElementById("output-text");
|
||||
outputElement = document.getElementById("output-text"),
|
||||
animation = document.getElementById("output-loader-animation");
|
||||
|
||||
if (value) {
|
||||
this.manager.controls.hideStaleIndicator();
|
||||
this.bakingStatusTimeout = setTimeout(function() {
|
||||
|
||||
// Start a timer to add the Bombe to the DOM just before we make it
|
||||
// visible so that there is no stuttering
|
||||
this.appendBombeTimeout = setTimeout(function() {
|
||||
animation.appendChild(this.bombeEl);
|
||||
}.bind(this), 150);
|
||||
|
||||
// Show the loading screen
|
||||
this.outputLoaderTimeout = setTimeout(function() {
|
||||
outputElement.disabled = true;
|
||||
outputLoader.style.visibility = "visible";
|
||||
outputLoader.style.opacity = 1;
|
||||
this.manager.controls.toggleBakeButtonFunction(true);
|
||||
}.bind(this), 200);
|
||||
} else {
|
||||
clearTimeout(this.bakingStatusTimeout);
|
||||
// Remove the Bombe from the DOM to save resources
|
||||
this.outputLoaderTimeout = setTimeout(function () {
|
||||
try {
|
||||
animation.removeChild(this.bombeEl);
|
||||
} catch (err) {}
|
||||
}.bind(this), 500);
|
||||
outputElement.disabled = false;
|
||||
outputLoader.style.opacity = 0;
|
||||
outputLoader.style.visibility = "hidden";
|
||||
@@ -494,6 +524,24 @@ class OutputWaiter {
|
||||
magicButton.setAttribute("data-original-title", "Magic!");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handler for extract file events.
|
||||
*
|
||||
* @param {Event} e
|
||||
*/
|
||||
async extractFileClick(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const el = e.target.nodeName === "I" ? e.target.parentNode : e.target;
|
||||
const blobURL = el.getAttribute("blob-url");
|
||||
const fileName = el.getAttribute("file-name");
|
||||
|
||||
const blob = await fetch(blobURL).then(r => r.blob());
|
||||
this.manager.input.loadFile(new File([blob], fileName, {type: blob.type}));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default OutputWaiter;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user