Skip to content

Commit

Permalink
Add armoring support
Browse files Browse the repository at this point in the history
  • Loading branch information
v3rm0n committed Feb 4, 2024
1 parent 843b4dc commit 6daa4a3
Show file tree
Hide file tree
Showing 17 changed files with 331 additions and 38 deletions.
44 changes: 29 additions & 15 deletions bin/dage.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:io';

import 'package:args/args.dart';
import 'package:collection/collection.dart';
import 'package:dage/dage.dart';
import 'package:logging/logging.dart';

Expand Down Expand Up @@ -33,21 +34,22 @@ void main(List<String> arguments) async {
throw Exception('At least one recipient needed!');
}
if (isPassphraseEncryption) {
final encrypted = encryptWithPassphrase(readFromInput(results));
writeToOut(results, encrypted);
final encrypted = encryptWithPassphrase(await readFromInput(results));
await writeToOut(results, encrypted);
} else {
final encrypted = encrypt(readFromInput(results), keyPairs.toList());
writeToOut(results, encrypted);
final encrypted =
encrypt(await readFromInput(results), keyPairs.toList());
await writeToOut(results, encrypted);
}
} else if (results['decrypt']) {
final identityList = results['identity'] as List<String>;
if (identityList.isNotEmpty) {
final identities = await getIdentities(results);
final decrypted = decrypt(readFromInput(results), identities);
writeToOut(results, decrypted);
final decrypted = decrypt(await readFromInput(results), identities);
await writeToOut(results, decrypted);
} else {
final decrypted = decryptWithPassphrase(readFromInput(results));
writeToOut(results, decrypted);
final decrypted = decryptWithPassphrase(await readFromInput(results));
await writeToOut(results, decrypted);
}
}
} catch (e, stacktrace) {
Expand All @@ -66,21 +68,29 @@ Future<List<AgeKeyPair>> getIdentities(ArgResults results) async {
return keyPairs.toList();
}

Stream<List<int>> readFromInput(ArgResults results) {
Future<Stream<List<int>>> readFromInput(ArgResults results) async {
if (results.rest.isNotEmpty) {
final fileName = results.rest.last;
return File(fileName).openRead();
final file = File(results.rest.last);
if (await isArmored(file)) {
final bytes = await file.openRead().toList();
return Stream.value(armorDecoder.convert(bytes.flattened.toList()));
}
return file.openRead();
} else {
return stdin;
}
}

void writeToOut(ArgResults results, Stream<List<int>> bytes) {
Future<void> writeToOut(ArgResults results, Stream<List<int>> bytes) async {
final output = results['output'];
if (output != null) {
File(output).openWrite().addStream(bytes);
if (results['armored']) {
final bytesList = await bytes.toList();
bytes = Stream.value(armorEncoder.convert(bytesList.flattened.toList()));
}
await File(output).openWrite().addStream(bytes);
} else {
stdout.addStream(bytes);
await stdout.addStream(bytes);
}
}

Expand All @@ -103,10 +113,14 @@ ArgResults parseArguments(List<String> arguments) {
abbr: 'r', help: 'Encrypt to the specified RECIPIENT. Can be repeated.');
parser.addMultiOption('identity',
abbr: 'i', help: 'Use the identity file at PATH. Can be repeated.');
parser.addFlag('armored',
abbr: 'a',
negatable: false,
help: 'Write the result as an armored file.');

final results = parser.parse(arguments);

if (results['usage']) {
if (results['usage'] || results.arguments.isEmpty) {
stdout.writeln(parser.usage);
stdout.writeln('''
Expand Down
3 changes: 2 additions & 1 deletion lib/dage.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export 'package:dage/src/encrypt.dart';
export 'package:dage/src/header.dart';
export 'package:dage/src/keypair.dart';
export 'package:dage/src/plugin/plugin.dart';
export 'package:dage/src/plugin/encoding.dart';
export 'package:dage/src/encoding.dart';
export 'package:dage/src/stanza.dart';
export 'package:dage/src/passphrase_provider.dart';
export 'package:dage/src/armor.dart';
91 changes: 91 additions & 0 deletions lib/src/armor.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math' as math;

import 'package:collection/collection.dart';
import 'package:dage/src/armor_parser.dart';

const _label = 'AGE ENCRYPTED FILE';
const _armorStart = '-----BEGIN $_label-----';
const _armorEnd = '-----END $_label-----';

final _codec = AgeArmorCodec();

class AgeArmorCodec extends Codec<List<int>, String> {
@override
Converter<String, List<int>> get decoder => AgeArmorDecoder();

@override
Converter<List<int>, String> get encoder => AgeArmorEncoder();
}

class AgeArmorDecoder extends Converter<String, List<int>> {
@override
List<int> convert(String input) {
final result = decode(input);
if (result.isEmpty) {
throw NoArmorBlockFoundException._(input);
}
return result.first;
}

List<List<int>> decode(String armoredString) {
final trimmed = armoredString.trim();
if (!trimmed.startsWith(_armorStart)) {
throw Exception('Armored file not valid');
}
if (!trimmed.endsWith(_armorEnd)) {
throw Exception('Armored file not valid');
}
final result = <List<int>>[];
for (final matches in armorParserMatches('$trimmed\n')) {
final preLabel = matches[0];
final data = matches[1];
final postLabel = matches[2];

if (preLabel != postLabel || _label != preLabel) {
print('WHAT');

This comment has been minimized.

Copy link
@ErkoRisthein

ErkoRisthein Feb 6, 2024

Member

@v3rm0n 🤣

continue;
}

result.add(base64.decode(data));
}
return result;
}
}

class AgeArmorEncoder extends Converter<List<int>, String> {
@override
String convert(List<int> input) {
final s = StringBuffer();
s.writeln(_armorStart);
final lines = base64.encode(input);
for (var i = 0; i < lines.length; i += 64) {
s.writeln(lines.substring(i, math.min(lines.length, i + 64)));
}
s.writeln(_armorEnd);
return s.toString();
}
}

Future<bool> isArmored(File file) async {
final header = await file.openRead(0, _armorStart.length).toList();
final eq = const ListEquality().equals;
return eq(header.flattened.toList(), _armorStart.codeUnits);
}

Converter<List<int>, List<int>> get armorDecoder =>
utf8.decoder.fuse(_codec.decoder);

Converter<List<int>, List<int>> get armorEncoder =>
_codec.encoder.fuse(utf8.encoder);

class NoArmorBlockFoundException implements Exception {
final String data;

NoArmorBlockFoundException._(this.data);

@override
String toString() => 'No valid armor blocks were found in the data:\n$data';
}
74 changes: 74 additions & 0 deletions lib/src/armor_parser.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright 2019 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import 'package:petitparser/petitparser.dart';

final _cr = char('\x0d');
final _lf = char('\x0a');
final _eol = ignore((_cr & _lf) | _cr | _lf);

final _preeb =
(string('-----BEGIN ') & string('AGE ENCRYPTED FILE') & string('-----'))
.pick(1);
final _posteb =
(string('-----END ') & string('AGE ENCRYPTED FILE') & string('-----'))
.pick(1);
final _base64char = pattern('a-zA-Z0-9+/');
final _base64pad = char('=');

final _base64line = _base64char.times(64) & _eol;

final _base64singlePadSuffix = (_base64char.repeat(3) & _base64pad & _eol);
final _base64doublePadSuffix =
(_base64char.repeat(2) & _base64pad.repeat(2) & _eol);

final _base64finl = _base64char.repeat(4).repeat(1, 15) &
(_base64singlePadSuffix | _base64doublePadSuffix | _eol) |
_base64singlePadSuffix |
_base64doublePadSuffix;

final _base64text = flatten(
(_base64line.plus() & _base64finl) | _base64line.star() | _base64finl);
final armorParser =
(_preeb & _eol & _base64text & _posteb & _eol).permute([0, 2, 3]);

Iterable<List> armorParserMatches(String armoredString) {
return armorParser.allMatches(armoredString, overlapping: false);
}

void _flattenString(dynamic value, StringBuffer target) {
if (value == null) {
return;
}
if (value is String) {
target.write(value);
return;
}
if (value is List) {
for (final v in value) {
_flattenString(v, target);
}
return;
}
throw ArgumentError('Unsupported type ${value.runtimeType}');
}

/// Create a [Parser] that ignores output from [p] and return `null`.
Parser<String?> ignore<T>(Parser<T> p) => p.map((_) => null);

/// Create a [Parser] that flattens all strings in the result from [p].
Parser<String> flatten(Parser<dynamic> p) => p.map((value) {
final s = StringBuffer();
_flattenString(value, s);
return s.toString();
});
21 changes: 21 additions & 0 deletions lib/src/decrypt.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ library age.src;
import 'dart:typed_data';

import 'package:async/async.dart';
import 'package:collection/collection.dart';
import 'package:cryptography/cryptography.dart';
import 'package:dage/src/armor.dart';
import 'package:dage/src/stream.dart';
import 'package:logging/logging.dart';

Expand All @@ -16,6 +18,16 @@ final Logger _logger = Logger('AgeDecrypt');

const _macSize = 16;

Stream<List<int>> decryptArmored(
Stream<List<int>> content, List<AgeKeyPair> keyPairs,
{PassphraseProvider passphraseProvider =
const PassphraseProvider()}) async* {
final bytes = await content.toList();
yield* decrypt(
Stream.value(armorDecoder.convert(bytes.flattened.toList())), keyPairs,
passphraseProvider: passphraseProvider);
}

Stream<List<int>> decrypt(Stream<List<int>> content, List<AgeKeyPair> keyPairs,
{PassphraseProvider passphraseProvider =
const PassphraseProvider()}) async* {
Expand Down Expand Up @@ -43,6 +55,15 @@ Stream<List<int>> decrypt(Stream<List<int>> content, List<AgeKeyPair> keyPairs,
symmetricFileKey: symmetricFileKey);
}

Stream<List<int>> decryptArmoredWithPassphrase(Stream<List<int>> content,
{PassphraseProvider passphraseProvider =
const PassphraseProvider()}) async* {
final bytes = await content.toList();
yield* decryptWithPassphrase(
Stream.value(armorDecoder.convert(bytes.flattened.toList())),
passphraseProvider: passphraseProvider);
}

Stream<List<int>> decryptWithPassphrase(Stream<List<int>> content,
{PassphraseProvider passphraseProvider =
const PassphraseProvider()}) async* {
Expand Down
2 changes: 1 addition & 1 deletion lib/src/plugin/encoding.dart → lib/src/encoding.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import 'dart:typed_data';
String base64RawEncode(List<int> data) =>
base64Encode(data).replaceAll('=', '');

List<List<int>> chunk(List<int> data, int chunkSize) {
List<List<int>> chunked(List<int> data, int chunkSize) {
final chunked = <List<int>>[];
for (var i = 0; i < data.length; i += chunkSize) {
final end = (i + chunkSize < data.length) ? i + chunkSize : data.length;
Expand Down
2 changes: 1 addition & 1 deletion lib/src/header.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import 'package:collection/collection.dart';
import 'package:cryptography/cryptography.dart';
import 'package:logging/logging.dart';

import 'plugin/encoding.dart';
import 'encoding.dart';
import 'passphrase_provider.dart';
import 'stanza.dart';
import 'stream.dart';
Expand Down
2 changes: 1 addition & 1 deletion lib/src/plugin/scrypt.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import 'package:pointycastle/pointycastle.dart';
import '../keypair.dart';
import '../passphrase_provider.dart';
import '../stanza.dart';
import 'encoding.dart';
import '../encoding.dart';
import 'plugin.dart';

class ScryptPlugin extends AgePlugin {
Expand Down
2 changes: 1 addition & 1 deletion lib/src/plugin/x25519.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import 'package:logging/logging.dart';
import '../keypair.dart';
import '../passphrase_provider.dart';
import '../stanza.dart';
import 'encoding.dart';
import '../encoding.dart';
import 'plugin.dart';

class X25519AgePlugin extends AgePlugin {
Expand Down
2 changes: 1 addition & 1 deletion lib/src/stanza.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import 'package:cryptography/cryptography.dart';

import 'keypair.dart';
import 'passphrase_provider.dart';
import 'plugin/encoding.dart';
import 'encoding.dart';
import 'plugin/plugin.dart';

abstract class AgeStanza {
Expand Down
8 changes: 8 additions & 0 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.9.0"
petitparser:
dependency: "direct main"
description:
name: petitparser
sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750
url: "https://pub.dev"
source: hosted
version: "5.4.0"
pointycastle:
dependency: "direct main"
description:
Expand Down
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ dependencies:
convert: ^3.1.1
cryptography: ^2.7.0
logging: ^1.2.0
petitparser: ^5.4.0
pointycastle: ^3.7.4

dev_dependencies:
Expand Down
7 changes: 7 additions & 0 deletions test/armored
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBURWlGMHlwcXIrYnB2Y3FY
TnlDVkpwTDdPdXdQZFZ3UEw3S1FFYkZET0NjCkVtRUNBRWNLTituL1ZzOVNiV2lW
K0h1MHIrRThSNzdEZFdZeWQ4M253N1UKLS0tIFZuKzU0anFpaVVDRStXWmNFVlkz
ZjFzcUhqbHUvejFMQ1EvVDdYbTdxSTAK7s9ix86RtDMnTmjU8vkTTLdMW/73vqpS
yPC8DpksHoMx+2Y=
-----END AGE ENCRYPTED FILE-----
Empty file added test/not_armored
Empty file.
Loading

0 comments on commit 6daa4a3

Please sign in to comment.